VYPR
Medium severity5.9NVD Advisory· Published May 27, 2026· Updated May 27, 2026

CVE-2026-45027

CVE-2026-45027

Description

WeGIA is a web manager for charitable institutions. In versions prior to 3.7.3, when a user logs in, html/login.php hashes the submitted password using PHP's hash() function with the SHA-256 algorithm and no salt before comparing it to the stored value. The password change flow in controle/FuncionarioControle.php follows the same pattern. SHA-256 is a general-purpose cryptographic hash built for speed, not password storage. Without a salt, identical passwords produce identical digests, making the entire hash database vulnerable to a single precomputed rainbow table lookup. This vulnerability is fixed in 3.7.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

WeGIA uses unsalted SHA-256 for password hashing, enabling offline recovery of plaintext passwords via rainbow tables or brute force.

Vulnerability

WeGIA, a web manager for charitable institutions, stores user password digests using the SHA-256 algorithm without a salt in the html/login.php file (lines 47-48) for login verification and in controle/FuncionarioControle.php (lines 542-544) for password changes. Because SHA-256 is a fast, general-purpose cryptographic hash and no salt is added, identical passwords produce identical digests, making the entire hash database vulnerable to precomputed rainbow table attacks. Versions prior to 3.7.3 are affected [1].

Exploitation

An attacker must first obtain the hashed password values from the pessoa table through a separate vulnerability (e.g., SQL injection via the memorando module, as demonstrated in the advisory with an XML injection technique using updatexml to extract 64-character hashes in chunks [1]). Once the attacker possesses the unsalted SHA-256 hashes, they can perform offline password cracking at extremely high speed (billions of attempts per second) using hardware such as GPUs, or look up precomputed rainbow tables for SHA-256, with no rate limiting or per-user salt to slow the attack [1].

Impact

An attacker who successfully recovers the plaintext password for a user, especially an administrator, can impersonate that user within WeGIA. This can lead to unauthorized access to sensitive operational data, manipulation of charitable institution records, and further compromise of the application. The lack of salt means the compromise of a single password can directly reveal the passwords of all other users with the same password (since identical hashes correspond to identical passwords) [1].

Mitigation

The vulnerability is fixed in WeGIA version 3.7.3 [1]. Institutions running earlier versions should upgrade immediately to this patched release. No workaround is provided in the available references; the fix likely migrates from hash('sha256', ...) to a properly salted, slow password hashing function such as password_hash and password_verify (as hinted at in a comment in the source code [1]). There is no indication that this CVE appears on the CISA KEV list.

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Wegia/Wegiainferred2 versions
    <3.7.3+ 1 more
    • (no CPE)range: <3.7.3
    • (no CPE)range: <3.7.3

Patches

1
fa373a5036a2

fix: Weak Password hashing algorithm [Security https://github.com/LabRedesCefetRJ/WeGIA/security/advisories/GHSA-hcgv-vmq6-j6qg]

https://github.com/labredescefetrj/wegiaGabrielPintoSouzaApr 28, 2026Fixed in 3.7.3via llm-release-walk
4 files changed · +108 43
  • classes/LoginHelper.php+53 0 added
    @@ -0,0 +1,53 @@
    +<?php
    +
    +class LoginHelper
    +{
    +    public static function hashPassword(string $password): string
    +    {
    +        return password_hash($password, PASSWORD_DEFAULT);
    +    }
    +
    +    public static function verifyPassword(string $password, ?string $storedHash): bool
    +    {
    +        return self::verifyAndMigrate($password, $storedHash)['valid'];
    +    }
    +
    +    public static function verifyAndMigrate(string $password, ?string $storedHash): array
    +    {
    +        if (!is_string($storedHash) || $storedHash === '') {
    +            return [
    +                'valid' => false,
    +                'needs_rehash' => false,
    +                'updated_hash' => null,
    +            ];
    +        }
    +
    +        if (self::isModernHash($storedHash)) {
    +            $valid = password_verify($password, $storedHash);
    +
    +            return [
    +                'valid' => $valid,
    +                'needs_rehash' => $valid && password_needs_rehash($storedHash, PASSWORD_DEFAULT),
    +                'updated_hash' => $valid && password_needs_rehash($storedHash, PASSWORD_DEFAULT)
    +                    ? self::hashPassword($password)
    +                    : null,
    +            ];
    +        }
    +
    +        $legacyHash = hash('sha256', $password);
    +        $valid = hash_equals(strtolower($storedHash), strtolower($legacyHash));
    +
    +        return [
    +            'valid' => $valid,
    +            'needs_rehash' => $valid,
    +            'updated_hash' => $valid ? self::hashPassword($password) : null,
    +        ];
    +    }
    +
    +    private static function isModernHash(string $storedHash): bool
    +    {
    +        $info = password_get_info($storedHash);
    +
    +        return !empty($info['algo']);
    +    }
    +}
    
  • controle/FuncionarioControle.php+39 39 modified
    @@ -3,11 +3,12 @@
         if (session_status() === PHP_SESSION_NONE)
    
             session_start();
    
     
    
    -require_once dirname(__FILE__, 2) . DIRECTORY_SEPARATOR . 'config.php';
    
    -require_once dirname(__FILE__, 2) . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'Csrf.php';
    
    -include_once ROOT . "/dao/Conexao.php";
    
    -include_once ROOT . '/classes/Funcionario.php';
    
    -include_once ROOT . '/classes/QuadroHorario.php';
    
    +require_once dirname(__FILE__, 2) . DIRECTORY_SEPARATOR . 'config.php';
    +require_once dirname(__FILE__, 2) . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'Csrf.php';
    +require_once ROOT . '/classes/LoginHelper.php';
    +include_once ROOT . "/dao/Conexao.php";
    +include_once ROOT . '/classes/Funcionario.php';
    +include_once ROOT . '/classes/QuadroHorario.php';
     include_once ROOT . '/dao/FuncionarioDAO.php';
    
     include_once ROOT . '/dao/QuadroHorarioDAO.php';
    
     include_once ROOT . '/dao/PermissaoDAO.php';
    
    @@ -533,39 +534,38 @@ public function verificarExistente()
             return $funcionario;
    
         }
    
     
    
    -    public function verificarSenha()
    
    -    {
    
    -        try {
    
    -            extract($_REQUEST);
    
    -            $nova_senha = hash('sha256', $nova_senha);
    
    -            $confirmar_senha = hash('sha256', $confirmar_senha);
    
    -            $senha_antiga = hash('sha256', $senha_antiga);
    
    -            if ($nova_senha != $confirmar_senha) {
    
    -                return 1;
    
    -            }
    
    -            else {
    
    -                $pdo = Conexao::connect();
    
    -                $funcionarioDAO = new FuncionarioDAO();
    
    -                $senha = $funcionarioDAO->getSenhaByIdPessoa($id_pessoa);
    
    -
    
    -                if ($senha != $senha_antiga) {
    
    -                    return 2;
    
    -                }
    
    -            }
    
    -            return 3;
    
    -        }
    
    +    public function verificarSenha()
    +    {
    +        try {
    +            extract($_REQUEST);
    +            if ($nova_senha != $confirmar_senha) {
    +                return 1;
    +            }
    +            else {
    +                $funcionarioDAO = new FuncionarioDAO();
    +                $senha = $funcionarioDAO->getSenhaByIdPessoa((int) $id_pessoa);
    +                $passwordCheck = LoginHelper::verifyAndMigrate($senha_antiga, $senha);
    +
    +                if (!$passwordCheck['valid']) {
    +                    return 2;
    +                }
    +
    +                if ($passwordCheck['updated_hash'] !== null) {
    +                    $funcionarioDAO->alterarSenha((int) $id_pessoa, $passwordCheck['updated_hash']);
    +                }
    +            }
    +            return 3;
    +        }
             catch (Exception $e) {
    
                 Util::tratarException($e);
    
             }
    
         }
    
    -    public function verificarSenhaConfig()
    
    -    {
    
    -        extract($_REQUEST);
    
    -        $nova_senha = hash('sha256', $nova_senha);
    
    -        $confirmar_senha = hash('sha256', $confirmar_senha);
    
    -        if ($nova_senha != $confirmar_senha) {
    
    -            return 1;
    
    -        }
    
    +    public function verificarSenhaConfig()
    +    {
    +        extract($_REQUEST);
    +        if ($nova_senha != $confirmar_senha) {
    +            return 1;
    +        }
             else {
    
                 return 3;
    
             }
    
    @@ -1026,10 +1026,10 @@ public function alterarSenha()
                 if (!preg_match($regex, $nova_senha))
    
                     throw new InvalidArgumentException('A senha informada não atende aos requisitos mínimos estabelecidos.', 412);
    
     
    
    -            $nova_senha = hash('sha256', $nova_senha);
    
    -            if (isset($redir)) {
    
    -                $page = $redir;
    
    -                $verificacao = $this->verificarSenhaConfig();
    
    +            $nova_senha = LoginHelper::hashPassword($nova_senha);
    +            if (isset($redir)) {
    +                $page = $redir;
    +                $verificacao = $this->verificarSenhaConfig();
                 }
    
                 else {
    
                     $verificacao = $this->verificarSenha();
    
    @@ -1315,4 +1315,4 @@ public function excluir()
                 Util::tratarException($e);
    
             }
    
         }
    
    -}
    \ No newline at end of file
    +}
    
  • dao/FuncionarioDAO.php+1 1 modified
    @@ -583,7 +583,7 @@ public function retornaId($cpf)
     
         public function getSenhaByIdPessoa(int $idPessoa)
         {
    -        $stmt = $$this->pdo->prepare("SELECT senha FROM pessoa where id_pessoa=:idPessoa");
    +        $stmt = $this->pdo->prepare("SELECT senha FROM pessoa where id_pessoa=:idPessoa");
             $stmt->bindValue(':idPessoa', $idPessoa, PDO::PARAM_INT);
             $stmt->execute();
     
    
  • html/login.php+15 3 modified
    @@ -6,6 +6,7 @@
     require_once '../Functions/funcoes.php';
     require_once './seguranca/sessionStart.php';
     require_once '../classes/Util.php';
    +require_once '../classes/LoginHelper.php';
     
     try {
         if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    @@ -42,14 +43,25 @@
             exit;
         }
     
    -    //Verificação de senha - Estudar uma migração para password_verify com retrocompatibilidade para usuários antigos
    -	$pwd = hash('sha256', $senha);
    +    $passwordCheck = LoginHelper::verifyAndMigrate($senha, $usuario['senha'] ?? null);
     
    -    if ($pwd != $usuario['senha']) {
    +    if (!$passwordCheck['valid']) {
             header("Location: ../index.php?erro=erro");
             exit;
         }
     
    +    if ($passwordCheck['updated_hash'] !== null) {
    +        $updateStmt = $pdo->prepare("
    +            UPDATE pessoa
    +            SET senha = :senha
    +            WHERE id_pessoa = :id_pessoa
    +        ");
    +        $updateStmt->bindValue(':senha', $passwordCheck['updated_hash']);
    +        $updateStmt->bindValue(':id_pessoa', $usuario['id_pessoa'], PDO::PARAM_INT);
    +        $updateStmt->execute();
    +        $usuario['senha'] = $passwordCheck['updated_hash'];
    +    }
    +
         //Proteção contra Session Fixation
         session_regenerate_id(true);
     
    

Vulnerability mechanics

Root cause

"Use of unsalted SHA-256 (a fast, general-purpose hash) for password storage instead of a slow, salted password hashing algorithm like bcrypt."

Attack vector

An attacker who obtains the `pessoa` table (e.g., via SQL injection, as demonstrated in the PoC using XPATH error-based extraction) can recover plaintext passwords offline at billions of attempts per second because all passwords are stored as unsalted SHA-256 hashes [ref_id=1]. Without a salt, identical passwords produce identical digests, making the entire hash database vulnerable to a single precomputed rainbow table lookup [ref_id=1]. The attacker can then authenticate as any account, including administrators, by using the recovered plaintext password [ref_id=1].

Affected code

The vulnerability exists in `html/login.php` (lines 47-48) and `controle/FuncionarioControle.php` (lines 542-544), where passwords are hashed using PHP's `hash('sha256', ...)` with no salt [ref_id=1]. The patch introduces a new `classes/LoginHelper.php` class that uses `password_hash()` with `PASSWORD_DEFAULT` and provides a `verifyAndMigrate()` method to transition legacy unsalted SHA-256 hashes to modern bcrypt hashes on successful login [patch_id=2713913].

What the fix does

The patch replaces all direct `hash('sha256', ...)` calls with `LoginHelper::hashPassword()` (which uses `password_hash($password, PASSWORD_DEFAULT)`, i.e., bcrypt) and `LoginHelper::verifyAndMigrate()` [patch_id=2713913]. The `verifyAndMigrate()` method checks whether the stored hash is a modern bcrypt hash via `password_get_info()`; if it is a legacy unsalted SHA-256 hash, it computes `hash('sha256', $password)` and compares with `hash_equals()`, then transparently re-hashes the password with bcrypt and updates the database row [patch_id=2713913]. This provides backward compatibility while migrating all users to a slow, salted password hashing algorithm that resists offline brute-force and rainbow-table attacks.

Preconditions

  • inputAttacker must obtain the pessoa table (e.g., via SQL injection or database breach)
  • authNo authentication required to exploit the weak hashing once hashes are obtained
  • networkNetwork access to the WeGIA application is needed for the SQL injection extraction step

Reproduction

Prerequisites: an authenticated session with access to the memorando module. Step 1 — Extract password hashes from the `pessoa` table via XPATH error-based SQL injection. MariaDB truncates XPATH error output to 32 characters, so 25-character SUBSTRING chunks are used, requiring 3 requests for a full 64-character SHA-256 hash. Request 1: `POST /WeGIA/controle/control.php?id_memorando=1'+AND+updatexml(1,concat(0x7e,SUBSTRING((SELECT+concat(cpf,0x3a,LENGTH(senha),0x3a,senha)+FROM+pessoa+LIMIT+1),1,25),0x7e),1)--+-` with body `nomeClasse=DespachoControle&metodo=listarTodos&modulo=memorando` → returns `'admin:64:9dcc9cbd309bfe6'`. Request 2 — positions 25-49: `...SUBSTRING(...),25,25)...` → `'3101c96687fb79ca847e9f238'`. Request 3 — positions 50-73: `...SUBSTRING(...),50,25)...` → `'ce965f82eb44e8daf825cdbb'`. Reconstructed hash: `9dcc9cbd309bfe63101c96687fb79ca847e9f238ce965f82eb44e8daf825cdbb`. Step 2 — Verify the hash is unsalted SHA-256: `echo -n "wegia" | sha256sum` matches exactly, confirming no salt [ref_id=1].

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.