Resources
Domain:
my-beta.us.opswat.com
product.my-beta.us.opswat.com
: most of the API calls happen here.
Documentation:
- Introduction - MetaDefender IT Access: helps me alot in Secure Access configuration.
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 of2sAZEURKPurzV1qd212h4ysxrqf16iLa4lKz7HLm
iv
:6f70737761746d657461616363657373
, which is the hex representation ofopswatmetaaccess
Activation Link
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&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
}