On Wed, Jul 16, 2025 at 10:16:47PM +0000, Andy Tinkham wrote:
> On July 15, 2025, CyberArk disclosed 5 vulnerabilities in our Conjur OSS 
> product.
> 
>   * CVE-2025-49827<https://www.cve.org/CVERecord?id=CVE-2025-49827> - 
> Critical - Bypass of IAM Authenticator in Secrets Manager, Self-Hosted 
> (formerly Conjur Enterprise) and Conjur OSS (GitHub 
> Advisory<https://github.com/cyberark/conjur/security/advisories/GHSA-gmc5-9mpc-xg75>)
>   * CVE-2025-49828<https://www.cve.org/CVERecord?id=CVE-2025-49828> - High - 
> Remote Code Execution in Secrets Manager, Self-Hosted (formerly Conjur 
> Enterprise) and Conjur OSS (GitHub 
> Advisory<https://github.com/cyberark/conjur/security/advisories/GHSA-93hx-v9pv-qrm4>)
>   * CVE-2025-49829<https://www.cve.org/CVERecord?id=CVE-2025-49829> - Medium 
> - Missing validations in Secrets Manager, Self-Hosted (formerly Conjur 
> Enterprise) and Conjur OSS (GitHub 
> Advisory<https://github.com/cyberark/conjur/security/advisories/GHSA-9w76-m74g-4c2r>)
>   * CVE-2025-49830<https://www.cve.org/CVERecord?id=CVE-2025-49830> - High - 
> Path traversal and file disclosure in Secrets Manager, Self-Hosted (formerly 
> Conjur Enterprise) and Conjur OSS (GitHub 
> Advisory<https://github.com/cyberark/conjur/security/advisories/GHSA-7m6h-fqrm-m9c5>)
>   * CVE-2025-49831<https://www.cve.org/CVERecord?id=CVE-2025-49831> - 
> Critical - IAM Authenticator Bypass via Mis-configured Network Device in 
> Secrets Manager, Self-Hosted (formerly Conjur Enterprise) and Conjur OSS 
> (GitHub 
> Advisory<https://github.com/cyberark/conjur/security/advisories/GHSA-952q-mjrf-wp5j>)

> All users of Conjur OSS are encouraged to update to the 1.22.1 release, 
> available on 
> DockerHub<https://hub.docker.com/layers/cyberark/conjur/1.22.1/images/sha256-331fecd01c5a8a6179165bedba57b85f7cd1283b6b2a9a4f29fcb1e7a92580b3>
>  and at the GitHub.com Conjur 1.22.1 
> release<https://github.com/cyberark/conjur/releases/tag/v1.22.1>.  These 
> issues also affect our Secrets Manager, Self-Hosted (formerly Conjur 
> Enterprise) product and have been disclosed to our customers in our security 
> bulletin CA25-22<https://www.cyberark.com/CA25-22>.
> For further information, please see our blog 
> post<https://www.cyberark.com/resources/product-insights-blog/addressing-recent-vulnerabilities-and-our-commitment-to-security>.

Thank you for sharing this with oss-security!

There's now also a disclosure by Cyata, the researchers who found these
issues:
https://cyata.ai/blog/exploiting-a-full-chain-of-trust-flaws-how-we-went-from-unauthenticated-to-arbitrary-remote-code-execution-rce-in-cyberark-conjur/

They also looked for and found logic flaws in HashiCorp Vault, but I am
hoping we'll have a separate thread on that (I am asking them to post).

Meanwhile, attached is a plain text export of the above blog post.

Alexander
                      © 2025 Cyata. All rights reserved.

   August 6, 2025

   • 22 min read

  Exploiting a full chain of trust flaws: how we went from unauthenticated to
            arbitrary remote code execution (RCE) in CyberArk Conjur

   Written by Yarden Porat, Core Team Engineer

Introduction

   Enterprise vaults are designed to secure the secrets, credentials, and
   tokens that control access to everything else. That’s what makes them
   such prime targets for attackers.

   When they succeed at exploiting them, the results can be severe, including
   enterprise-wide credential theft, data tampering and leakage, operational
   disruption, and regulatory exposure.

   This is why it was so important for us at Cyata to understand just how
   secure these vaults really are.

   Over several weeks of focused research, we analyzed two widely used
   enterprise secrets management platforms, HashiCorp Vault and CyberArk
   Conjur. We invite you to read the full breakdown of our findings regarding
   HashiCorp Vault.

   In Conjur, our investigation uncovered a full pre-authentication remote
   code execution (RCE) chain. Through a series of logic flaws, we
   demonstrated how to achieve RCE on a Conjur deployment using the default
   AWS integration setup.

   No credentials. No tokens. Not even a real AWS account. Just a carefully
   crafted series of requests that moved from zero access to full control,
   all by exploiting default behavior.

   This exploit chain doesn’t rely on memory corruption or race conditions.
   It’s entirely logic-based, combining type confusion flaws and a new
   attack primitive that bypasses multiple layers of trust enforcement –
   all using standard requests and default configurations.

   It’s the kind of path that’s invisible to traditional defenses and
   devastating once it’s exploited.

   In this post, we walk through what we found, how we found it, and what it
   means for the infrastructure Conjur is trusted to protect.

   These issues have since been fixed by the vendor. To stay protected,
   update to the latest version as soon as possible.

What is Conjur

   CyberArk Conjur is an open-source secrets management solution designed to
   securely store, manage, and control access to sensitive credentials, API
   keys, certificates, and other secrets used in DevOps and cloud-native
   environments.

   Built for automation-first infrastructure, Conjur is primarily used to
   manage machine and AI identities, and broker secure access between
   services in CI/CD pipelines, Kubernetes clusters, and other dynamic
   environments.

   It integrates with tools like Jenkins, Kubernetes, Ansible, and Terraform,
   providing policy-based access controls and scalable machine-to-machine
   authentication.

   Its policy engine allows for precise permission scoping and enforcement,
   and its compatibility with enterprise CyberArk deployments makes it
   appealing for hybrid cloud security strategies.

   In many organizations, Conjur serves as a trust anchor in automated
   workflows, and as this research shows, a compromise in Conjur can have
   far-reaching consequences across systems that depend on it.

   Conjur Highlights

     * Designed to secure secrets across DevOps pipelines and CI/CD
       environments
     * Provides centralized secrets management with encryption
     * Offers role-based access control (RBAC) for securing machine and
       application identities
     * Integrates with container orchestration platforms and developer tools,
       e.g., Kubernetes, Jenkins, and Ansible
     * Open source and commercial options

   For more on Conjur, see here.

Methodology

   Before we began searching for vulnerabilities, we first set out to
   understand how Conjur works. Rather than poking around randomly, we
   focused on its internal design:

     * How authentication flows are implemented
     * How permissions are enforced
     * How identity is modeled

   We examined the policy language, the structure of resource identifiers,
   and the way secrets are stored and retrieved.

   The first breakthrough came when we reviewed how Conjur handles AWS IAM
   authentication. From there, we noticed a pattern – critical HTTP
   endpoints often rely on attacker-controlled inputs like :id, :account, and
   :kind, without a consistent validation mechanism.

   The lack of validation made us think there was a deeper path to explore.
   So instead of chasing standalone issues, we asked ourselves what a full
   compromise might require, then worked backward to find the flaws that
   could make it possible.

The path to compromise

   Step 1: Redirecting trust – IAM Authentication Bypass

   We began by exploring Conjur’s AWS IAM authentication flow, a widely
   used mechanism that lets workloads authenticate without needing hardcoded
   credentials. It's commonly used in CI/CD pipelines, where machines need to
   retrieve secrets securely.

   This integration is quite complex. The AWS instance generates a signed
   Authorization header, which Conjur passes to AWS Security Token Service
   (STS). STS verifies the signature and returns the identity of the AWS
   instance.

   But we noticed something interesting in the implementation. AWS runs STS
   in multiple regions, each with its own endpoint (like
   sts.us-east-1.amazonaws.com). Conjur doesn’t determine the region on its
   own, it extracts it from user-supplied parameters. That small detail
   opened the door to bypassing the verification entirely.

   What we looked for

   We wanted to understand how Conjur decides which STS endpoint to target
   during IAM authentication.

   In AWS, signed requests are region-specific, which means that a request
   signed for us-east-1 must be verified against sts.us-east-1.amazonaws.com.
   So, the key question became - how does Conjur determine the region used in
   the original signed request?

   To answer that question, we search for and located the method in
   Conjur’s codebase responsible for parsing this value:
   extract_sts_region.

   What we found

   The extract_sts_region function attempts to parse the region from either
   the Host or the Authorization header:

 def extract_sts_region(signed_headers)
   host = signed_headers['host']

   if host == 'sts.amazonaws.com'
     return 'global'
   end

   match = host&.match(%r{sts\.([\w\-]+)\.amazonaws\.com})
   return match.captures.first if match

   match = 
signed_headers['authorization']&.match(%r{Credential=[^/]+/[^/]+/([^/]+)/})
   return match.captures.first if match

   raise Errors::Authentication::AuthnIam::AWSHeaders, 'Failed to extract AWS 
region from authorization headers'
 end

   The region is then used to construct the STS domain:

 def aws_call(region:, headers:)
 host = if region == 'global'
 'sts.amazonaws.com'
 else
 "sts.#{region}.amazonaws.com"
  End
  aws_request = 
URI("https://#{host}/?Action=GetCallerIdentity&Version=2011-06-15";)
 begin
 @client.get_response(aws_request, headers)
  rescue StandardError => e
 # Handle any network failures with a generic verification error 
raise(Errors::Authentication::AuthnIam::VerificationError, e)
 End
  end

   The extract_sts_region function uses unvalidated, attacker-controlled
   header content to construct the STS domain. It lacks proper validation for
   special URL characters. For example, if we send:

 Authorization: Credential= A/A/attacker.domain?/

   the region is extracted as attacker.domain?, and Conjur constructs the STS
   endpoint as:

 https://sts.attacker.domain?.amazonaws.com

   By using the question mark (?) symbol, we were able to strip off the
   .amazonaws.com portion of the URL. This is critical - because in domain
   verification, only the last part of the domain actually matters.

   Conjur then sends a verification request to that domain, which means that
   we fully control the endpoint it trusts to validate IAM identities.

   To demonstrate the impact, we stood up a mock STS server at sts.cyata.ai,
   programmed it to return a forged but well-formed GetCallerIdentity
   response, and watched Conjur accept it as valid.

   No AWS credentials is needed for this step. Just logic.

   Why it matters

   This bypass fundamentally breaks Conjur’s trust boundary.

   By redirecting validation to an attacker-controlled STS endpoint, we were
   able to impersonate any AWS identity we wanted without supplying a single
   credential.

   This was the first step in the chain, where a completely unauthenticated
   attacker could now enter the system, appearing to be a legitimate AWS
   identity.

   Step 2: Creating arbitrary resource – Host Factory abuse

   After bypassing IAM authentication, we took a step back to examine how
   Conjur works behind the scenes, focusing on its policy model and internal
   data structures.

   We looked at how it identifies and manages core resources like hosts,
   variables, policies, and groups, all of which are stored in an internal
   PostgreSQL database.

   Each resource in the database is indexed using a composite ID made up of
   three parts:

     * Account – the name of the Conjur account
     * Kind – the type of resource (host, user, variable, policy, etc.)
     * Identifier – a unique name for the specific resource.

   Here is an example of the resources table:

   This structure isn’t just used internally, it’s also exposed
   externally. Most of Conjur’s HTTP endpoints accept :account, :kind, and
   :id as URL parameters and rely on them to locate or manipulate resources.

   Once we understood how identifiers like :account, :kind, and :id were
   used, we had a strong feeling that if we could create a resource with a
   controlled identifier, it would matter.

   We didn’t yet know how to do it, or whether it was even possible, but it
   felt like the kind of primitive worth chasing.

   We started examining different endpoints, testing for anything that might
   let us influence identifiers. Eventually, we found what we needed in the
   Host Factory - an endpoint that lets you create a host with an arbitrary
   identifier

   What we looked for

   We specifically investigated what permissions are required to use the Host
   Factory.

   First, we considered what the standard Host Factory flow is supposed to
   look like:

    1. Call POST /host_factory_tokens with the ID of a known host factory to
       get a token
    2. The /host_factory_tokens verifies that we have execute permissions on
       the specified host factory resource.
    3. Use that token to call POST /host_factories/hosts, supplying an ID for
       the new host

   With this in mind, we wanted to answer two critical questions:

     * How can we use the Host Factory endpoint if no host factory resource
       is configured?
     * How can we obtain execute permissions on a host factory?

   What we found

   Host Factory Kind Mismatch

   To get a Host Factory token, the caller must have execute permission on
   the target resource. But Conjur doesn’t verify that the resource is
   actually of kind host_factory.

   That means an attacker can pass any resource they have execute permission
   on, such as a group or layer, and still receive a valid token.

   This opens up a clear abuse path, where basically if we own a group
   (ownership gives all permission on a resource), we can:

     * Call the Host Factory token endpoint with that group’s ID
     * Get a token
     * Use it to create a host

   If a client owns a group resource, they can abuse the Host Factory flow to
   create a new host with an arbitrary identifier. And since they own the
   resource used to obtain the token, they also become the owner of the newly
   created host.

   Why it matters

   We can now create hosts with arbitrary identifiers. This is a powerful
   primitive, one that opens the door to attacking some of Conjur’s core
   and most sensitive endpoints.

   And the requirement? Just ownership of a group, a small foothold,
   considering the control it unlocks.

   Step 3 – WhoAmI Policy? - Connecting the Steps

   But how can we become the owner of a group?

   Would a common configuration be enough? Probably, but we wanted to assume
   as little as possible. So, here’s the only assumption we made: AWS
   authentication is enabled.

   That means we needed to find a default path to group ownership. To do
   that, we revisited the IAM configuration from Conjur’s official
   documentation.

 # policy id needs to match the convention `conjur/authn-iam/<service ID>`
 - !policy
   id: conjur/authn-iam/prod
   body:
     - !webservice

     - !group clients

     - !permit
       role: !group clients
       privilege: [ read, authenticate ]
       resource: !webservice

   In the example policy, we see a group called clients. If we can become the
   owner of that group, the rest of the chain falls into place.

   What we looked for

   We started by asking: Can we use the AWS bypass to authenticate as
   user:admin? In this configuration admin is the owner of our policy, and
   therefore the owner of the group.

   What made us think this was possible?

   We noticed that Conjur doesn’t validate the kind of identity during
   authentication. That raised an interesting possibility: what if we could
   authenticate as a user instead of a host?

   But a restrictive check stood in the way: IAM allows authentication as a
   user only if the resource_id contained a slash (/).

   That meant user:admin was off the table.

   What we found

   The ability to authenticate as a user turned out to be just the tip of the
   iceberg.

   The AWS IAM bypass actually lets us authenticate as any resource.

   Why? Because all resources in Conjur are stored in the same PostgreSQL
   table, and the permissions system is global across all types.

   There’s no distinction between identity resources (host, user) and role
   resources (group, layer, policy)- they’re all treated the same by the
   authorization engine.

   This means we can go one step further:

     * Authenticate as the policy itself.
     * And in our case, that policy is the owner of the clients group.

   Why it matters

   This step changed the game. It bridged the gap between unauthenticated
   access and a meaningful attack surface in Conjur.

   By combining a forged STS response, a policy identity instead of a host,
   and flaws in the Host Factory flow, we gained the ability to mint new
   hosts with names and ownership entirely under our control.

   Step 4 – Executing code - abusing policy templates for RCE

   With the ability to create arbitrary hosts through the Host Factory, we
   started thinking about what we could do with that control.

   At first, we focused on privilege escalation, maybe gaining access to
   restricted variables or impersonating higher-privileged roles. But the
   deeper we looked into how Conjur structures policy and manages secrets,
   the more dangerous the path became.

   That’s when we found the Policy Factory, a mechanism that lets you apply
   reusable policy templates programmatically.

   But these templates aren’t static YAML files. They support ERB (Embedded
   Ruby), which means every time a template is applied, it gets rendered and
   executed dynamically.

   And that raised a critical question.

   What if we could control what gets executed?

   What we looked for

   We knew ERB was being executed - the question was whether we could
   influence it.

   To find out, we focused on three key questions:

     * Where are policy templates stored?
     * How does Conjur know which resource is a valid template?
     * Is there any way to inject ERB into that process?

   Answering these questions meant diving into how templates are applied
   behind the scenes, and into how Conjur loads them, renders them, and
   evaluates them through its API.

   What we found

   Policy Factory

   The Policy Factory is triggered through a dedicated endpoint:

 POST "/factories/:account/:kind(/:version)/:id"

   This endpoint is a bit confusing. It takes the account, kind, version, and
   id parameters, then searches for a matching policy template under the
   conjur/factories branch.

   Note: The kind parameter does not refer to the resource type - the
   resource is always expected to be a variable. Instead, kind is used purely
   as part of the lookup path within the conjur/factories namespace.

   The relevant code looks like this:

 def find(kind:, id:, account:, role:, version: nil)
   factory = if version.present?
     @resource["#{account}:variable:conjur/factories/#{kind}/#{version}/#{id}"]
   else
     @resource.where(
       Sequel.like(
         :resource_id,
         "#{account}:variable:conjur/factories/#{kind}/%"
       )
     ).all
   end
 end

   This code locates a variable resource - but the template itself is stored
   inside the associated secret. In the next step, the code takes the
   resource_id of the variable, uses it to look up thesecret, and then
   decrypts its value from the secrets table.

   That decrypted value is the policy template.

   ERB in Policy Templates

   Why do we care about policy templates?

   Because these templates aren’t just static YAML files, rather they’re
   ERB-rendered. Ruby code can be embedded and executed every time a template
   is applied.

   For example, here’s a policy template from CyberArk documentation:

 module PolicyTemplates
   class CreateHost < PolicyTemplates::BaseTemplate
     def template
       <<~TEMPLATE
       - !host
         id: "<%= id %>"
         <% unless annotations.nil? || annotations.empty? %>
         annotations:
         <% annotations.each do |key, value| %>
           <%= key %>: <%= value %>
         <% end %>
         <% end %>
       TEMPLATE
     end
   end
 end

   That’s why, at first, we considered finding a way to abuse an existing,
   configured template. But, Conjur has no built-in templates. Each one must
   be defined manually.

   So we asked a bigger question: instead of abusing an existing template,
   what if we could control the entire template?

   That would allow us to inject arbitrary Ruby code, and have it executed.

   No race conditions. No memory corruption.

   Assigning a Secret to a Host

   So how can we create a policy template?

   As noted earlier, policy templates are stored in the secrets table, and
   secrets are supposed to be assigned only to variables.

   But we discovered a critical gap: nothing actually enforces that rule.

   That meant we could assign a secret containing ERB to a resource of kind
   host.

   For example, consider the following resource_id:

   MyAccount:host:conjur/factories/my_template

   We were able to assign a secret to this resource, even though it is a
   host, not a variable.

   Injecting a host as a variable

   There was still one missing piece. How could we make Conjur treat our host
   as a policy template?

   After all, policy templates are supposed to be stored as variables.

   Let’s take another look at the code:

 module PolicyTemplates
   class CreateHost < PolicyTemplates::BaseTemplate
     def template
       <<~TEMPLATE
       - !host
         id: "<%= id %>"
         <% unless annotations.nil? || annotations.empty? %>
         annotations:
         <% annotations.each do |key, value| %>
           <%= key %>: <%= value %>
         <% end %>
         <% end %>
       TEMPLATE
     end
   end
 end

   We can see that the expected resource_id looks like this:

   “{Account}:variable:conjur/factories/{kind}”.

   Here there is a specific check that the kind is variable. So, what can we
   do?

   As mentioned earlier, there are several cases of missing validation. And
   here, there is no validation on the account parameter.

   We can use the HTTP parameter account as both the account and the kind
   component of the resource_id.

   Therefore, we can call the policies endpoint, with account =
   “MyAccount:host”.

   This results in a resource_id of:

   MyAccount:host:variable:conjur/factories/{kind}.

   So, if we create an arbitrary host with an identifier of:

 “variable:conjur/factories/”

   Then our host will have a full resource_id of:

   “MyAccount:host:variable:conjur/factories/{kind}.”.

   This means, the search will work, and our created secret will be rendered
   as a policy template, executing arbitrary code.

   Why it matters

   This was the final step, and the one that delivered full remote code
   execution.

   The code ran because:

     * We tricked Conjur into fetching a host as if it were a variable
     * We assigned ERB as a secret
     * And Conjur executed it, exactly as it was designed to

   This vulnerability turned arbitrary host creation into arbitrary command
   execution.

   It was a seamless, start-to-finish exploit chain that went from
   unauthenticated access to root.

Putting it all together: Full exploit chain

   Now that we had each vulnerability mapped, it was time to connect the
   dots.

   We had fully compromised Conjur’s authorization and access controls. And
   with that, we were able to execute the full exploit chain.

   Here’s how it unfolded:

   1. IAM authentication bypass

   We injected a fake region in the AWS-signed request, causing Conjur to
   send the STS validation call to a server we controlled (e.g.,
   sts.cyata.ai).

   That gave us the power to forge valid-looking GetCallerIdentity responses,
   impersonating any identity we wanted.

   2. Authenticate as a policy

   Instead of impersonating a host (as the IAM flow expects), we
   authenticated as a policy resource, which Conjur accepted under its
   default IAM configuration.

   This immediately gave us elevated privileges, by design.

   3. Abuse the Host Factory

   Now acting as a policy, we invoked the POST /host_factory_tokens endpoint,
   passing a group resource we controlled.

   Because Conjur didn’t verify the resource kind, we received a valid
   token, despite never using a real host factory.

   4. Create a host named like a variable

   With the token, we created a new host and crafted its resource_id to look
   like a policy template path.

 /variable/conjur/factories/malicious_template

   This host was now positioned to impersonate a valid policy template.

   5. Attach ERB code as a secret

   Conjur is supposed to create secrets on variables only, but that
   enforcement was missing.

   So, we assigned a malicious ERB payload directly to the host, the same
   host that would later be treated as a template.

   6. Trigger ERB execution

   Finally, we called the Policy Factory endpoint. It looked up the template
   by resource_id, found our crafted host, extracted the attached ERB, and
   executed it, exactly as designed.

   Final result

   This exploit chain moved from unauthenticated access to full remote code
   execution without ever supplying a password, token, or AWS credentials.

   Every step used default behavior. Nothing looked out of place. Until it
   was too late.

Responsible disclosure

   Throughout this research, Cyata adhered to a thorough and responsible
   disclosure process. We engaged CyberArk early, providing detailed
   technical reports for each finding - from the initial authentication
   bypass to the full exploit chain.

   The collaboration was transparent, focused, and genuinely constructive.

   CyberArk’s security team responded with professionalism and clarity,
   working closely with us to verify the issues and implement timely fixes
   ahead of public disclosure. Their responsiveness, openness, and strong
   communication set a high bar - and we sincerely wish more vendors handled
   security research this way.

   Kudos to the team at CyberArk – and congratulations on the recent
   acquisition.

   Disclosure timeline

     * May 23, 2025 - We submitted the initial disclosure to CyberArk,
       detailing the IAM authentication bypass.
     * May 29, 2025 - We followed up with the full exploit chain,
       demonstrating end-to-end impact.
     * June 5, 2025 - We held a joint meeting with CyberArk’s security team
       to ensure all issues were clearly communicated.
     * June 5–19, 2025 - We engaged in several rounds of technical
       discussion to review proposed fixes. These were minor follow-ups, no
       additional critical issues were found.
     * June 19, 2025 - Five CVEs were issued.

Conclusion

   Secrets vaults sit at the heart of modern infrastructure. They secure the
   credentials, tokens, and keys that power everything else. That’s exactly
   what makes them so valuable, and so vulnerable.

   At Cyata, we see ensuring the resilience of these systems as one of the
   most critical cybersecurity mandates today. That’s why we invest in
   deep, targeted vulnerability research not just to uncover flaws, but to
   help organizations build trust into the very core of their environments.

   By proactively probing and challenging the systems that manage identity,
   access, and secrets, we aim to reduce risk, prevent breaches, and
   strengthen the foundations of the digital world.

   And we’re just getting started

Reply via email to