Resources

Domain:

  • my-beta.us.opswat.com
  • product.my-beta.us.opswat.com: most of the API calls happen here.

Documentation:

Tasks

- [ ] Identify new features
	- [ ] Secure Access > Protected Applications
	- [ ] Secure Access > Access Methods
	- [ ] Inventory > Services
	- [ ] Policies > File Security
	- [ ] Policies > Playbooks
	- [ ] User Management > Roles
	- [ ] User Management > SSO
	- [ ] License
- [x] Tools: 
	- [x] `Rustscan`, `Web-Cache-Vulnerability-Scanner` on the main URL.
	- [x] `js-beautify`, `trufflehog`, `webcrack` on JavaScript files.
- [ ] Setup "Protected Application" feature
- [x] Check the account registration flow
- [x] Check reset password flow
- [ ] Get APIs that have query params and brute-force for hidden query params.

Recon

Npm Audit

dompurify  <3.2.4
Severity: moderate
DOMPurify allows Cross-site Scripting (XSS) - https://github.com/advisories/GHSA-vhxf-7vqr-mrjg
fix available via `npm audit fix --force`
Will install jspdf@3.0.0, which is a breaking change
node_modules/dompurify
  jspdf  2.0.0 - 2.5.2
  Depends on vulnerable versions of dompurify
  node_modules/jspdf

Rustscan

.\rustscan.exe -a console.fusion-staging.opswat.com
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Scanning ports faster than you can say 'SYN ACK'
 
[~] The config file is expected to be at "C:\\Users\\quan.m.le\\.rustscan.toml"
Open 108.157.32.51:80
Open 108.157.32.51:44

Findings

Encrypted JWT

The token returned from the login request is encrypted:

1yRZRHOWMYWF+kM0DkaPo0to3VWMSuPiBnYm2Y9hE1kYbWiBWmWxfqKiib+IdkrpG6h3rBFj80SXH5neRB+HolXX32MjHZX56Hjqsacf983HgukmXml6zUR8jG4qQM8V4a7ZrFCHpTN8Fvyygo71rSi3dbbNQVx5ouIQ8rD00eoJKKW3D1QI+XjAWhZSBfiWrNQRAfdtzdG2VdKdWaITJBgaRdQgskUKsfEMnPcSz77veypZEhOFMUlW/jMhUvGVntuxn1HJ1mJzRcGfuGr1c24i+0g4zBzGv5lyNBMRlgt73Rxm/ZJE3BWnbHRXNeTQ2uuNsU5np0Iw9Jp0om/PK2A5D4KmNx639l+S8Gruw63/N2L1rRfe1R9HeqocL+bVJFTzAPSem0x1zBPOK06UP+Z2mBHXsJffdfKleKrZ5jyGVa6dI0eURVMUehAItgAHgR8B8gUGS94C+bZEYKYrnt1PQQZor5R4QaeNTZWLZBeIdp8ZXCc1q8mFk6y4Ji1+f0UaMsarXrc1Po6+FsDjtmka7IlETGCCYsbzbc4fko49qjpgPBeyFvh5/LYHhBAdW/Nrpyc/PdHu3km1BHjtW7Fw3IJ2NSDAn/aDolf8cQjuJPZANQixYrAElMT9DNeszrsMkMMNC3LFEhNsTm0GmRCSZ3Ww1IUVz1upGCVFeiVI49MZffAzdQDZL1M9jTHtpEirSzdD/ZGNhcNZK384Nw75RkxfTsSmlu2qSnE+3ePK5fMR+dQe+BOY0ozZJUT14Eu7SncBuy9IN+02Yfuy9/uSTwW9KK+NhCI7hIb93N56U/WSArNygluv0IpUAw==

And is decrypted by this function in the client-side scripts:

decrypt(e) {
  if (e === "null" || e.trim().length < 1) {
	return "";
  } else {
	return y.AES.decrypt(e.toString(), this.keyDecrypt, {
	  iv: this.iv,
	  mode: y.mode.CFB,
	  padding: y.pad.NoPadding
	}).toString(y.enc.Utf8);
  }
}

About how to discover this function, I have used Eval Villain browser extension for logging data passed into common sinks. And the sink related to the token is decodeURIComponent. Follow the trace and I found those functions that will eventually invoke the decrypt function:

getEncryptedJwtToken() {
  const e = localStorage.getItem('token');
  return decodeURIComponent(e)
}
getJwtToken() {
  const e = this.getEncryptedJwtToken();
  return this.decrypt(e)
}
getJwtDecodedToken(e) {
  if (!this.getJwtTokenSg || e) {
	const t = this.getJwtToken();
	this.getJwtTokenSg = t
  }
  return this.decodeJwt(this.getJwtTokenSg)
}

Additionally, in the same file of the above functions, I have found how the key and IV are constructed:

this.keyDecrypt = y.enc.Hex.parse((0, y.MD5) (this.environment.keyDecrypt).toString()),
this.iv = y.enc.Utf8.parse('opswatmetaaccess'),

After debugging, I have discovered the values of the key and the IV:

  • keyDecrypt: 1ebd340a3a990f487fc05e5d369d3e9b, which is the MD5 hash of 2sAZEURKPurzV1qd212h4ysxrqf16iLa4lKz7HLm
  • iv: 6f70737761746d657461616363657373, which is the hex representation of opswatmetaaccess

The app parameter in the request used for sending confirming email:

{
  "email" : "quan.m.le+4@opswat.com",
  "query" : {
    "app" : "appFS0001'''"
  }
}

Has been reflected to the link in the email:

https://id.opswat.com/active?code=365768&email=H4sIAAAAAAAAAxXKwRHAMAgDsJWg1HEYh9Sw%2Fwi56i1EWZqwnUNNBTia2f1VhSpdjuOvrDkLT%2BvAF1GwvzEv7RYmF0AAAAA%3D&app=appFS0001'''&redirect=https%3A%2F%2Fconsole.metaaccess-c.opswat.com&samlrequest=null&SAMLRequest=null&SigAlg=null&Signature=null

I did try to inject the redirect field for controlling the redirect URL after login.

{
  "email" : "quan.m.le+4@opswat.com",
  "query" : {
    "app" : "appFS0001",
    "redirect" : "https://www.google.com"
  }
}
However, the `redirect` field is not reflected to the link.

I also try to inject the redirect query param to the value of the app field:

{
  "email" : "quan.m.le+4@opswat.com",
  "query" : {
    "app" : "appFS0001&redirect=https://www.google.com"
  }
}
However, the `&` character is HTML-encoded.

When sending too many requests with the same email:

{
  "statusCode" : 400,
  "code" : 40057,
  "name" : "limitExceededException",
  "lockTime" : 0,
  "remains" : 0
}

SAML Authentication

There is a request used for inititating SAML authentication:

GET /console/saml2/authenticate/fusion HTTP/2
Host: fusion-c.opswat.com
Cookie: __opswat-session-login=undefined; __opswat-refresh-login=undefined; __opswat-payload-login=undefined; __opswat-login=true
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Referer: https://id.opswat.com/

The response is a server-side redirect:

HTTP/2 302 Found
Date: Fri, 14 Mar 2025 06:29:59 GMT
Content-Length: 0
Location: https://id.opswat.com/login?app=appMA0001&redirect=https%3A%2F%2Fproduct.my-beta.us.opswat.com%2Fconsole&SAMLRequest=lVJLb6MwEL7vr0CW2hvgOAlpvIGItoq2UqtmA91Db8YMrSWwWY%2FJdv%2F9Oi9teolUyT7MeL6H9c1i%2BdG1wRYsKqNTMoooCUBLUyv9lpKXchXekGX2bYGia1nP88G96w38HgBdkCOCdR53ZzQOHdgC7FZJeNk8puTduR55HPfW1IN0Ufc3rMCJaMDI9PhHuEiaLpYeaVqId%2FRxUTzHolUC4xoaMbQu3xUkuPdiSgu3d3jiVfU5T2velF6Kvk%2F9fcoppaNr0fXfLdTKgnTpHnY1zq%2FYyp%2BLpvz70RYJVsZK2H86JY1o0bce7lOSb37CLJk1UoRJNa%2FDCZ1U4Q2biLCRrGpYVY%2BnYuZncS0Q1Rb%2BoxEHeNDohHYpYZRNQzoOR5OSJpzN%2BXQeJfPklQRra5yRpr1V%2BhDFYDU3AhVyLTpA7iQv8qdHziLKq8MQ8h9luQ7Xz0VJgl%2BnSNkuUh%2ByRn4I8TJXfxQm2SFzvndszxkuE4jTVpDsSzuwiM%2F1smP5eeWyfw%3D%3D&RelayState=7fcbb27a-21a5-4fe9-87ae-bcf80ced39b7&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=EXLcNhnezijeCmSFUpIGUA8GoYrb1YqQ678leDVoQXLZRxMMX4THJkQpZCh4hLAL39POEiSluJnxEYBI%2BiUJxQ27f5iEtfHJcnx39YaqPOhxNfJBAQFgHf35TyKAknNN0Msvb8%2FgWVD86Vw4UppGZwFikOEA4%2FnRjbVOGWawde1Za6HynMy8LIRxYrFmv1GbbHMEYdaZcMTsCSGoAG4WHDu1Sk7DOOtK5KvvCACkF259zuypp9G4LukYtcKo1tPmGdSZMISYwj1LmI0q8QVXAeIR53sJ%2F9IfZE5ERwjq8eiOAvaEg8YETJjMBXZmDu4cx%2Fbwf33v8hSLMjCWH3bSxg%3D%3D

Some query params of the above redirect:

  • app: appMA0001
  • redirect: https://product.my-beta.us.opswat.com/console
  • SAMLRequest
  • RelayState
  • SigAlg: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
  • Signature

The decoded SAML Request:

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest
  AssertionConsumerServiceURL="https://product.my-beta.us.opswat.com/console/saml/SSO/alias/defaultAlias"
  Destination="https://id.opswat.com/login?app=appMA0001&amp;redirect=https%3A%2F%2Fproduct.my-beta.us.opswat.com%2Fconsole"
  ForceAuthn="false" ID="ARQe767fca-6b9d-404b-824a-fc2bf2bd35a7"
  IsPassive="false" IssueInstant="2025-03-14T06:29:59.696Z"
  ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
  Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
  <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://product.my-beta.us.opswat.com/console</saml2:Issuer>
</saml2p:AuthnRequest>

The SAML Response will be sent to the following endpoint:

POST /console/saml/SSO/alias/defaultAlias HTTP/2
Host: gears-beta.opswat.com

The response has the following cookie:

Set-Cookie: JSESSIONID=EA31A1036CE9A75EC1DCDB693CDA3962; Path=/console; Secure; HttpOnly

It will be used in the following request for receiving token:

GET /console/my-saml/handle-saml-response HTTP/2
Host: gears-beta.opswat.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Cookie: JSESSIONID=EA31A1036CE9A75EC1DCDB693CDA3962

The token in the response:

HTTP/2 302 Found
Date: Fri, 14 Mar 2025 04:26:41 GMT
Content-Length: 0
Location: https://console.metaaccess-c.opswat.com/tcs?token=1yRZRHOWMYWF%2BkM0DkaPo0to3VWMSuPiBnYm2Y9hE1kYbWiBWmWxfqKiib%2BIdkrpG6h3rBFj80SXH5nddzCEovX5bZwgKFEAF7HFUkoVUWzyNHdKJb9SUjxlh%2BNV1Vc92gnqxKoy3YbiinP601gbii2eic3a7Kv%2FU6y0Q978oVNHManjCsAhZQK0pJKcCcRl3d7q1GrrGKOG5B1wBlUeTQ5WlXG6dYiymS0WEtSNtow9H5Y1fuOTiC0Fplo7wQAKP%2F%2Fkw02tjvH%2BHL0kkCiHm2NUqFMHJdBlUU94%2FLKPI2M9rlBb1Awwo%2Br59VzBpw%2Fzz8cH12mf%2BN3%2F%2F5U1Xn9jraUtdNfrYn%2FNWFZvS58feXvQ8PN8DO4JkHRpND4kOmiTUvZ0K8hCVQsTLZN7koa85qwlCHdEgGQKPQp5Ya3dHFpbtzCRYXGJwFIw09zqO68H2%2BEGrinuF5HTC9wRgs3W8Xp25%2BaxvkHGRKU3uQ0mRLAMJha6JvJ%2FjWEg9I9NepjXbYp53UESgde0eyQVoKEeSYb3bgSW3L04EM6qU%2BIzC%2FCXPZLrYO4nAVtethKZsrPLY2XJkc5qoV3neezWCj50O%2BXW3CfZgSXUXBC%2FVmF%2FVEOx6Jld2FNpVoYWn8Wt7D3JCywO4sAoOZJR34oGwAvegpIsynkCXQy7m6aI3mtLhV4zfXCh%2FH2AiVwixghSjyvZLEmSwK2Qba0zeYg5zV0bCBBelnwGETrXWlx6Zg0fJOwm2K9V0%2Bdfmliu8uUop%2FWJuagKeSmb1FHnQ1F0ySmY%2BE3hMM2UkUO9mGicuw3aSHB1qXwjl5G%2F9D8%2B6WoGAYqucFlW

The token is encrypted and we can decrypt via the approach mentioned in Encrypted JWT.

title: Fail: SAML Response Forging
I did try to conduct SAML Response Forging with omitted signatures/fake signature but no success as there is no `JSESSIONID` cookie in the response.

Hidden Params

Query params are CASE SENSITIVE.

Endpoint /fusion/console/groups. URL:

https://product.my-beta.us.opswat.com/fusion/console/groups?page=1&limit=25&sort=lastUpdated&order=asc

Hidden params:

  • search
  • Page
  • Filter

Endpoint /fusion/console/services. URL:

https://product.my-beta.us.opswat.com/fusion/console/services?page=1&limit=25&sort=serviceName&order=asc&productTypes=MDCORE&groupIDs=67d25771ea52ab7cbcd3a25d

Hidden params:

  • timezone
  • Page

Endpoint /pmp/inventory/policies. URL:

https://product.my-beta.us.opswat.com/pmp/inventory/policies?page=1&limit=25&order=asc

Hidden params:

  • search
  • sort
When using unclosed `(` or `)` in the `search` parameter of the `pmp/inventory/policies` URL, the response status code is 500. Moreover, when using `\u0000`, the response status code is also 500. This mean the value of the parameter is used in a JSON format, which can be MongoDB query.

Endpoint /pmp/inventory/mk5s. URL:

https://product.my-beta.us.opswat.com/pmp/inventory/mk5s?page=1&limit=25&sort=lastSeen&order=asc

Hidden params:

  • timezone
  • Page
  • Filter

Endpoint /pmp/inventory/kiosks. URL:

https://product.my-beta.us.opswat.com/pmp/inventory/kiosks?page=1&limit=25&sort=lastSeen&order=asc

Hidden params:

  • search
  • timezone
  • Page
  • groupIDs
  • Filter

Endpoint /mdd/inventory/policies. URL:

https://product.my-beta.us.opswat.com/mdd/inventory/policies?page=1&limit=25&order=asc

Hidden params:

  • search

Endpoint /mdd/inventory/devices. URL:

https://product.my-beta.us.opswat.com/mdd/inventory/devices?page=1&limit=25&sort=lastSeen&order=asc

Hidden params:

  • timezone
  • Page
  • Filter
The `Filter` param accepts a JSON. Moreover, if we use `';` as value, it still accepts. And if we use a number, it throws an exception like this:
 
~~~json
{
  "error" : "json: cannot unmarshal number into Go value of type mk5.Filter"
}
~~~

Open Redirects

The original request:

GET /mdd/inventory/vpack/resources/4.1.1/index.html?host_url=/mdd/inventory/vpack/config/183a8991-46a3-4d6d-9fca-21c95449f874/ HTTP/2
Host: product.my-beta.us.opswat.com

Request that will trigger a server-side redirect:

GET /mdd/inventory/vpack/resources/4.1.1?host_url=https://www.google.com HTTP/2
Host: product.my-beta.us.opswat.com

Its response:

HTTP/2 301 Moved Permanently
Date: Fri, 14 Mar 2025 08:47:36 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 104
Location: /mdd/inventory/vpack/resources/4.1.1/?host_url=https://www.google.com
 
<a href="/mdd/inventory/vpack/resources/4.1.1/?host_url=https://www.google.com">Moved Permanently</a>.
title: Todo: Try to exploit Reflected XSS

Commands & Tricks

Get JavaScript files from an URL:

Get-Content .\js.txt | ForEach-Object { Invoke-WebRequest -Uri $_ -OutFile ".\$(Split-Path $_ -Leaf)" }

MD Core Login Redirection

If the MD Core automatically redirects to SSO login page, we can change the response body of the request sent to /ssoready to this:

{
  "ready" : false
}