By Tao Sauvage
Overview
In his third SAP blog post, Anvil Secure's Director of Research, Tao Sauvage, explores how a low-speed fuzzing target became a focused custom harness and why the bug was serious even though a working RCE exploit was not achieved. Itโs a practical look at the value of persistence, careful root-cause analysis, and pulling on one technical thread until it leads somewhere unexpected.
Tao also presented this research at leHACK 2026. You can download the slides from his talk here.
I've written about SAP in the past: first, when presenting two local privilege escalation vulnerabilities identified during a customer engagement, and second, when discovering multiple vulnerabilities affecting SAP's SAPCAR archive utility tool.
In my last blog post, I ended on a cliffhanger: a potential pre-authentication vulnerability affecting most SAP software.
This is that thread. It starts with SAPCAR fuzzing, then SAP's CommonCryptoLib, and ends with CVE-2025-42940, which I could reach remotely through HANA's HDB X.509 authentication path.
I initially thought it could lead to RCE but I could not write a working exploit. I'm sharing my analysis, notes and attempts below.
Harness for black-box fuzzing
Last time, I started fuzzing the SAPCAR binary using AFL with Frida mode. I used the following command to perform black-box fuzzing:
taskset -c 0-3 afl-fuzz -i corpus -o findings -m none -O -- ./SAPCAR --tf @@
My target was, and still is, the signature validation performed against signed SAR archives. However, I mentioned the atrocious performance, with roughly 7 executions per second: SAPCAR needs to read the archive from the filesystem, parse the SAR format, decompress the file data, compute checksums and hashes, and finally compare them to the signed manifest.
Naturally, I wasn't satisfied with such low speed so I set out to focus the fuzzing against the signature verification library directly. I noted that SAPCAR loaded and used the libsapcrypto.so library to perform the cryptographic operations. I was not keen on reversing a crypto library so I set out to simply monitor the function calls and reproduce them in a custom harness.
Proxying library calls
For that purpose, I created a hack-ish tool named auto-shim-lib. Its purpose is very narrow: I pass it an arbitrary .so library and it spits out a shim library you can drop in its place. It parses the exported symbols, uses some assembly to save the stack, trace the calls and save the return values.
Good enough for my use case but not a general tracing framework (for instance, it doesn't support multithreaded binaries). In fact, it could be replaced with a 10-liner Frida script, but I was curious to write it myself to see how I would implement it.
Creating a shim library for libsapcrypto.so and running the SAPCAR utility to check the signature of a SAR archive gave the following:
LD_LIBRARY_PATH=. ASL_TARGET_LIB=./libsapcrypto_orig.so ./SAPCAR -tVf tp.sar
SAPCAR: processing archive tp.sar (version 2.01)
[+] asl lib loaded
[+] found target lib in ASL_TARGET_LIB env
[+] loaded target library ./libsapcrypto_orig.so
[+] function sapcr_set_property_int is being called
rdi: 7913600 (d) 78c080 (h)
rsi: 0 (d) 0 (h)
rdx: 38 (d) 26 (h)
rcx: 0 (d) 0 (h)
r8: 0 (d) 0 (h)
r9: 0 (d) 0 (h)
[+] return value
rax: 0 (d) 0 (h)
[+] function sapcr_set_property_int is being called
[...]
[+] function sapcr_get_property_int is being called
[...]
[+] function sapcr_get_version is being called
[...]
[+] function SsfEncode is being called
[...]
[+] function SsfVerify is being called
rdi: 811304096 (d) 305b84a0 (h)
rsi: 12 (d) c (h)
rdx: 1 (d) 1 (h)
rcx: 811091933 (d) 305847dd (h)
r8: 3410 (d) d52 (h)
r9: 811329536 (d) 305be800 (h)
[+] return value
rax: 0 (d) 0 (h)
[...]
[+] function SsfDELSigRcpSsfInfoList is being called
[...]
[+] function SsfDELSsfOctetstring is being called
[..]
srwxrwxr-x 300 22 Aug 2012 20:27 patches.mf
srwxrwxr-x 10575444 22 Aug 2012 19:48 tp
-rw------- 4922 23 Aug 2012 09:20 SIGNATURE.SMF
[+] function sapcr_done is being called
[...]
It's a quick and easy way to get the full list of functions called by SAPCAR when verifying an archive, in order, with some insights about the parameters and return values.
SSF and writing the harness
Searching online for things related to "libsapcrypto", "SsfVerify", "Secure Store and Forward", etc. led me to the following PDF document from 1999: "Secure Store & Forward (SSF) API Specifications. Version 1.0. Technical Documentation. Dr. J. Schneider Michael Friedrich."

This document contained all the information about the APIs' input, output, and data types: exactly what I needed for a custom harness without having to reverse engineer the library. Talk about ABI stability!
I ended up hardcoding everything that I could except the fuzzing data, which gave me the following fuzzing function:
void fuzz_one_input(char *buf, int len)
{
SAPRETURN rc = 0;
SsfOctetstring signed_data = (SsfOctetstring)buf;
SAP_INT signed_data_len = (SAP_INT)len;
SigRcpSsfInformationList signerResultList = NULL;
SsfOctetstring output_data = NULL;
SAP_INT output_len = 0;
rc = SsfVerify(format, format_len, TRUE, signed_data, signed_data_len, (SsfOctetstring)buffer_encoded, buffer_encoded_len, private_address_book, private_address_book_len, pab_password, pab_password_len, &signerResultList, &output_data, &output_len);
/* ... */
}
With this harness, I bumped the execution speed from 7 to about 2,000 executions per second:
gcc call_libsapcrypto.c -o call_libsapcrypto -L. -lsapcrypto -Wl,-rpath,. afl-fuzz -i corpus/ -o findings/ -O -- ./call_libsapcrypto
While still not great, it was a massive improvement that I was happy with. I tried speeding things up even more with AFL's persistent mode, among others, but it kept crashing. So, I decided that it was good enough for a trial run.
Crash triage
I let AFL run for a couple of days, and it reported multiple crashes, though something was off: when running the crash files against the harness, nothing happened...
I quickly suspected that most of the crashes reported by AFL were in fact Frida crashing. I triaged the actual crashes using a quick bash loop:
for f in ./crashes/id*; do
./call_libsapcrypto < "$f" &>/dev/null
if [ $? -eq 139 ]; then
echo "$f"
fi
done
This left me with a handful of crashes that I could then triage manually (and with the assistance of LLMs) using GDB Enhanced Features (GEF) to compare the backtraces. Here is one such example:
#0 0x00007ffff7a1aa64 in sec_enc_hex () from ./libsapcrypto.so #1 0x00007ffff7ab643a in AVA2Name () from ./libsapcrypto.so #2 0x00007ffff7ab6705 in RDName2Name () from ./libsapcrypto.so #3 0x00007ffff7ab69d5 in seccrypt__DName2Name () from ./libsapcrypto.so #4 0x00007ffff7ab77de in sec_DName_DName2Name () from ./libsapcrypto.so #5 0x00007ffff7a65d6b in sec_DName_print () from ./libsapcrypto.so #6 0x00007ffff7a8fadb in sec_ToBeSigned_print () from ./libsapcrypto.so #7 0x00007ffff7a09e4f in sec_CertificateImpl_print () from ./libsapcrypto.so #8 0x00007ffff7ac8f96 in verify_Certificate () from ./libsapcrypto.so #9 0x00007ffff7aca6ae in sec_verificationmodulecontext_verify_object () from ./libsapcrypto.so #10 0x00007ffff79aff76 in sec_VerifiedObject_verify () from ./libsapcrypto.so #11 0x00007ffff79ae9ee in sec_VerificationContext_check_Certificate3 () from ./libsapcrypto.so #12 0x00007ffff79af368 in sec_VerificationContext_check_Certificate2 () from ./libsapcrypto.so #13 0x00007ffff79af38b in sec_VerificationContext_check_Certificate1 () from ./libsapcrypto.so #14 0x00007ffff79275d1 in ssf_verify_TimeStampSignature () from ./libsapcrypto.so #15 0x00007ffff79250d9 in secssf_SsfVerifyEx () from ./libsapcrypto.so #16 0x00007ffff792625a in secssf_SsfVerify () from ./libsapcrypto.so #17 0x00007ffff78e4cf6 in Trace_SsfVerify () from ./libsapcrypto.so #18 0x00005555555555b5 in fuzz_one_input () #19 0x0000555555555775 in main ()
We start from SsfVerify, go through ssf_verify_TimeStampSignature, then through functions related to certificate, then something related to Distinguished Names (DN), and finally crashing in sec_enc_hex. Something going wrong in libsapcrypto when handling malformed certificates perchance?
Root cause analysis
The SAR signature includes an X.509 certificate, which is then parsed by libsapcrypto. The bug was not obvious. In fact, sec_enc_hex had nothing to do with it and the culprit was seccrypt__ASN1getLengthAndTag, which is called by seccrypt__DName2Name.
The purpose of the seccrypt__DName2Name function is to parse the Distinguished Names (DNs) in the X.509 certificate (like Subject or Issuer for example) and extract a list of offsets into the encoded DN, later used while processing its RDN/AVA components.
To extract those offsets, seccrypt__DName2Name relies on the seccrypt__ASN1getLengthAndTag function, whose purpose is to take in a DER-encoded bytestream, parse one element and return an offset to the first content byte and another offset to the next element.
Note: I use "DER bytestream" loosely here to refer to the certificate's ASN.1/TLV stream. Some examples I show are technically not valid DER bytes in the strict sense (more like BER) but the libsapcrypto still processes them.
DER primer
X.509 certificates use DER (Distinguished Encoding Rules) to encode the ASN.1 data (Abstract Syntax Notation One). DER encodes ASN.1 values as TLV (Type-Length-Value) triplets. Let's take the following DER bytestream:
02 01 05
It represents an INTEGER (Tag: 0x02) of size 1 (Length: 0x01) and value 5 (Value: 0x05). Simple.
Interestingly, DER encoding allows two different ways to specify the length: short and long form. The short form is the one presented above, where a single byte is used for a length of at most 127 bytes. Starting at 128 and above, the most significant bit of the first length byte is set, and the remaining 7 bits indicate how many following bytes encode the length.
Going back to our simple example, the same INTEGER element can be encoded as follows:
02 81 01 05 02 82 00 01 05 02 83 00 00 01 05 02 84 00 00 00 01 05 ...
DER stipulates that only the shortest form possible should be used, so the above is technically not valid. However, libsapcrypto does not enforce that.
Signed length
The libsapcrypto library allows up to 4 bytes for the long form. Looking more closely at how it processes the 0x84 long form (where 4 bytes are used), we see the following code:
undefined8 seccrypt__ASN1getLengthAndTag ( /* ... */ ) {
// [...]
// Handling 0x84 .. .. .. ..
// Logic is: len = (b1<<24) | (b2<<16) | (b3<<8) | b4
uVar1 = offset + 6;
if (buf_len < uVar1) {
return 0xa0200012;
}
*out_value_offset = uVar1;
value_len_accum = b4 + uVar1
+ (long)(int)((uint)b1 << 24) // [1]
+ (long)(int)((uint)b2 << 16);
len_octet = b3;
// [...]
*out_end_offset = value_len_accum + (long)(int)((uint)len_octet << 8);
// [...]
}
The seccrypt__ASN1getLengthAndTag function treats the 4-byte length as a signed value (as hinted by the (int) cast at [1] before being cast again to long when computing value_len_accum). It is even clearer when looking at the disassembly code:
MOVZX EAX, byte ptr [RDI + R10*1 + 0x1] ; b1 SHL EAX, 0x18 ; EAX = b1 << 24 (32-bit shift) CDQE ; RAX = sign_extend_32_to_64(EAX) ADD RDX, RAX ; end_offset += sign_extended(b1 << 24)
So a long-form encoded length like 0xffffffff is treated as a signed value, i.e., -1. This length is then added to the header-end offset. Therefore, it is possible for seccrypt__ASN1getLengthAndTag to return an offset to the next element earlier in the DER bytestream.
Breaking assumptions
Now, how does that lead to a crash? We need to focus on how the vulnerable function is used by seccrypt__DName2Name. Looking at the decompiled code, it boils down to:
uint32_t stack_array[20]; // local_a8
uint32_t *offsets = stack_array;
uint32_t count = 0;
do {
if (buflen <= cursor) { offsets[count] = (uint32_t)cursor; break; } offsets[count] = (uint32_t)cursor; // [1]
if (count + 1 == 20) { offsets = calloc((buflen >> 1) + 1, sizeof(uint32_t)); // [2]
memcpy(offsets, stack_array, 20 * sizeof(uint32_t));
}
status = seccrypt__ASN1getLengthAndTag(buf, buflen, cursor, &tmp, &cursor, &tag); // [3]
count++;
} while (status >= 0);
For each TLV parsed in the DER bytestream, the function stores the current cursor offset inside an array at [1]. It relies on the vulnerable seccrypt__ASN1getLengthAndTag function to parse the stream at [3].
For the first 20 items, seccrypt__DName2Name is using a stack-based buffer. Beyond that, it allocates a new buffer on the heap at [2] and copies the old array over. The size of the allocated heap buffer is based on the (critical) assumption that you can get at most size(derBuffer) / 2 elements. This is understandable since the smallest TLV triplet can be encoded using 2 bytes (i.e., 1 byte for the tag and 1 byte for a length of 0).
This is sound as long as the following underlying assumption holds: the stream only moves forward. However, as we've highlighted earlier, seccrypt__ASN1getLengthAndTag can return an offset to the next element earlier in the stream, which breaks the assumption, since now the same bytes can be parsed multiple times.
Let's take the simplest example that triggers an infinite loop (only showing the malformed TLV; not the whole bytestream):
AA 84 FF FF FF FA
We have an 0xAA element (the element type does not matter so I picked something recognizable), whose long-form length is encoded using 4 bytes (0x84) and contains 0xfffffffa.
When seccrypt__ASN1getLengthAndTag parses that TLV, it computes a length of -6 and sets the next-element offset back to the first byte of the TLV it just parsed. Next time seccrypt__ASN1getLengthAndTag is called, the same parsing happens. And so on, forever.
Meanwhile, seccrypt__DName2Name keeps saving the current cursor offset in the next slot of the heap-based array. It only stops when seccrypt__ASN1getLengthAndTag returns an error or it has reached the end of the buffer. But neither condition is triggered since it keeps going backward to process the same TLV. At some point, seccrypt__DName2Name writes past the allocated array and corrupts the heap data located after it.
While our bug was triggered via ssf_verify_TimeStampSignature (as the SAR manifest is signed using PKCS7-TSTAMP), it got me thinking: What else would also take a client certificate? Could it be triggered remotely?
From local to remote
I searched online for more information about libsapcrypto and found SAP's 2013 announcement for CommonCryptoLib, an attempt to consolidate cryptographic-related operations into a single library:

As ticked in the image above, SSF is handled by libsapcrypto, which is what SAPCAR is using. But the library also supports things like TLS, Kerberos and HSM. Quite a lot of responsibilities.
While not shown in the image, I thought about HANA's HDBSQL CLI (think of it as the MySQL CLI equivalent). Isn't it possible to provide a client certificate when authenticating? More searching and... yes! It is possible with the following command:
hdbsql -j -A -sslprovider openssl -Z authenticationMethods=x509 -Z authenticationX509=mycert.pem -n <target>:<port> "SELECT CURRENT_USER, CURRENT_SCHEMA FROM DUMMY;"
The authentication method tells HDBSQL to use X.509 certificates. A successful X.509 login may still require the usual server-side trust and user-mapping configuration, but that was not the important part here. I wanted to know whether HANA would parse my user-controlled certificate before authentication completed.
Then, I attached GDB to the target HANA process, placed my breakpoint, (crossed my fingers,) and executed the command:
Thread 83 "HanaWorker" hit Breakpoint 5, 0x00007f65dce50bda in seccrypt__ASN1getLengthAndTag () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so (gdb) backtrace #0 0x00007f65dce50bda in seccrypt__ASN1getLengthAndTag () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so #1 0x00007f65dce87a43 in seccrypt__DName2Name () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so #2 0x00007f65dce889a8 in sec_DName_DName2Name () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so #3 0x00007f65dcd268e9 in ssf_create_VerifySigResList () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so #4 0x00007f65dcd27a4d in secssf_SsfVerifyEx () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so #5 0x00007f65dcce1199 in Trace_SsfVerifyEx () from /usr/sap/HXE/HDB90/exe/libsapcrypto.so [...] #15 0x00007f661ff37015 in ptime::SessionHandler::handleNotDecodedMessage(Execution::Context&, Execution::JobNode&, SessionLayer::CommEvent*) () from /usr/sap/HXE/HDB90/exe/libhdbsqlsession.so #16 0x00007f65ea232af0 in SessionLayer::CommEventJob::run(Execution::Context&, Execution::JobObject&) () from /usr/sap/HXE/HDB90/exe/libhdbsessionlayer.so [...] #22 0x00007f660435e6ea in start_thread () from /lib64/libpthread.so.0 #23 0x00007f660182058f in clone () from /lib64/libc.so.6
We've got a hit! The backtrace confirmed that when authenticating using X.509 and passing our user-controlled certificate, the vulnerable seccrypt__ASN1getLengthAndTag function is reached. This time, while going through ssf_create_VerifySigResList instead of ssf_verify_TimeStampSignature.
Importantly, the vulnerable function was reached before certificate trust or user mapping mattered.
I wrote a quick Python MitM script to patch my self-signed certificate on the wire, because HDBSQL rejected the malformed certificate as invalid before sending it. The MitM introduced the infinite-loop example mentioned earlier in the certificate bytes sent to HANA, and the whole HANA process crashed due to a segmentation fault!
Exploit primitive and strategies
The obvious next step was trying to exploit the bug remotely. As I already mentioned: I failed. However, I still want to document the process as someone (or something -- winks at LLMs) could succeed where I did not.
Motivated by Sean Heelan's blog post "On the Coming Industrialisation of Exploit Generation with LLMs", I thought I could leverage LLMs to help. However, the constraints were too great for the latest models at the time: they had a hard time understanding the bug fully, and an even harder time suggesting exploit strategies. Mythos and Fable came out later, and based on the marketing around them could be up for the task, but I didn't get a chance to try them before access was suspended.
I think the main reason I failed was my test environment: the target process took about 20 minutes to restart on its own. If people have dealt with HANA in the past, they know that it takes a while to start (usually, you only do that once, not once per request like me). This made the exploitation process beyond tedious. With a tighter feedback loop, I suspect that LLMs would yield better and more interesting results.
Going back to my attempts: based on the root cause analysis, we know that we can trigger an infinite loop that linearly writes offsets past the buffer on the heap. What else could we do?
Monotonicity assumption
If you've noticed in the first backtrace I showed, the crash happened inside sec_enc_hex, but why? Could that be the key to exploit the vulnerability?
The reason is that the bug, in addition to breaking the size assumption, also breaks another assumption made by the library: monotonicity. By jumping backward, we can have an array of offsets where the offset at j + 1 is actually smaller than the one at j.
For instance, consider the following array where all offsets are in ascending order, except the last one:
[0 4 6 9 8]
Later on, the code does:
char *hex_buf = malloc((end - start) * 2 + 2); // [1] strcpy(hex_buf, "#"); sec_enc_hex(buf + start, end - start, hex_buf + 1); // [2]
The call to malloc allocates a buffer with the wrong size because end - start = 8 - 9 = -1, which, multiplied by 2 and adding 2 equals 0 at [1], for which malloc will return a valid pointer on my test system.
Then at [2], it calls sec_enc_hex with 8 - 9 = -1 as the second parameter:
void sec_enc_hex(uint8_t *src, size_t len, char *dst) {
for (size_t i = 0; i < len; i++) { dst[i*2] = hextable[src[i] >> 4];
dst[i*2 + 1] = hextable[src[i] & 0xf];
}
dst[len * 2] = '\0';
}
The length is treated as an unsigned value: passing -1 is therefore equivalent to passing SIZE_MAX, resulting in a for loop copying a huge amount of data (on our 64-bit target), which has the same undesirable effect of our previous infinite loop.
Maybe using multiple concurrent connections could have one trigger the sec_enc_hex function while the other benefits from the overwrite somehow, before the process crashes. And even then, it'd be restricted to hex characters only. This seemed too hard and finicky to me.
Multi-pass buffers
Could we somehow craft a buffer that cleanly stops while resulting in an out-of-bounds write? It turns out that yes, with a little trick we can play: multi-pass DER buffers.
Instead of jumping back to the start of the bytestream, we could jump with an offset. Because of the offset, the second pass would interpret the buffer in a different way. For example, let's consider the following DER bytestream:
01 01 01 [1] 01 01 01 ... 01 01 01 03 04 83 00 00 09 [2] 02 01 01 AA 84 FF FF FF E0 [3] 02 01 01 02 01 01
It starts at [1] with a bunch of 01 01 01 TLVs representing BOOLEANs (0x01) of size 1 and value 1. It then includes a BIT STRING (0x03) at [2] of size 4 and value 0x84 0x00 0x00 0x09. Then our malformed TLV at [3], which jumps back to the start plus 1, shifting the whole sequence.
During the second pass, the same DER bytestream now is interpreted as:
01 // Skipped 01 01 01 [1] 01 01 01 ... 01 01 03 [2] 04 83 00 00 09 02 01 01 AA 84 FF FF FF E0 [3] 02 01 01 02 01 01
It skips the first byte and starts like before with a series of BOOLEANs at [1] but then the last one of the series becomes a BOOLEAN of value 3 at [2] followed by an OCTET STRING (0x04) of size 9 and value 0x02 0x01 0x01 0xAA 0x84 0xFF 0xFF 0xFF 0xE0 at [3], effectively eating up or skipping our malformed TLV parsed during the first pass.
It is also possible to craft a bytestream that can be parsed 3 times with the same trick, where the second pass starts at offset 1, and the third pass at offset 2. For example:
01 01 01 01 01 01 03 04 81 0A 04 0E BB 84 FF FF FF D9 01 01 AA 84 FF FF FF D2 02 01 01 02 01 01
I'll leave how that malformed bytestream is parsed as an exercise for the reader. It was fun crafting it, and you can imagine extending the idea further and chaining them as well.
With this trick, we can parse a DER bytestream multiple times, while exiting cleanly. With careful offset computations, it is possible to target a specific slot past our offset array on the heap. Furthermore, we can also better control the value being written, since we could jump back to the stream offset we want to write to that slot.
However, we are still limited to writing a small integer value. I didn't see any obvious hard limit on the size of the sequence, so maybe a larger certificate could lead to larger offset values (maybe up to 0xffff), but it still seemed unrealistic to write a valid pointer value on a 64-bit target system. Remember: the overflow is linear so we can't just overwrite the lower part of the target pointer. We'll necessarily overwrite the upper part as well.
Additionally, with the payload shapes I tried, writing the same value twice meant jumping to the same offset twice, which reintroduced a loop I could not break out of.
Other dead-ends
I also chased a few other side effects, because I wasn't willing to give up yet.
For instance, I skipped over the fact that seccrypt__DName2Name later calls another function named RDName2Name. That function has the same general shape: store offsets in an array, relocate the array on the heap when needed, and keep calling seccrypt__ASN1getLengthAndTag.
So, could the outer function craft an array of offsets such that the inner function would yield a more powerful exploit primitive? I didn't find anything useful.
Another idea was to insert a poison byte in the DER bytestream that would only be reached on the second pass, causing the parser to exit through the error path instead of the happy path. It worked but didn't solve any of my problems related to weak exploit primitives.
I also looked at dn_append_cs, which is called by AVA2Name, which itself is called by RDName2Name. By breaking the monotonicity assumption, it is possible to get a -1-style size passed down that path too. Unfortunately, the downstream code was bounded enough that I could not turn it into something useful.
I'll spare you the details for your own sanity. Mine is already too far gone.
Targeting HDB's data
Considering our constraints, I think our best strategy would be to target data fields. One angle I considered was to target the HDB session data that lives in memory.
Hypothetically, using multiple connections, maybe connection A could try to execute a SQL query as SYSTEM before being authenticated, while connection B leverages the bug to overwrite data related to connection A's session before it gets rejected. Maybe that means targeting the HDB state machine. Maybe that means a flag that marks the session as valid or authenticated. Maybe none of this works. That was the idea, at least.
This strategy requires way more effort than I'm willing to invest, especially with a low chance of success in my opinion. It requires a deep understanding of the HDB protocol (which is, thankfully, documented (PDF)) as well as its structures in memory (which, unfortunately, need to be reverse-engineered).
I tried some LLM-assisted reversing, with Claude and a Ghidra MCP plugin, but the SAP libraries related to HDB are enormous and the progress was slow. I gave up. Let's see how future models tackle that on their own overnight.
Other protocols
Here, I've focused on the HDB protocol with X.509 authentication, as it was the easiest one for me to test on my local VM. However, as shown in the CommonCryptoLib diagram, other protocols are supported as well, including TLS, Kerberos, etc.
Maybe those protocols could yield more powerful primitives when being fed malformed DER bytestreams? For instance, TLS can be configured to require a client certificate. This could be an interesting target, although I don't believe that it is the default setting in SAP systems.
I didn't check myself. I had to stop somewhere.
Conclusion
Over the course of a series of three blog posts, I've presented local privilege escalation vulnerabilities affecting SAP systems, vulnerabilities affecting SAP's SAPCAR utility tool, and finally a bug in SAP's CommonCryptoLib that I could trigger remotely before authentication.
Considering how widely CommonCryptoLib is used across SAP products, this bug was especially serious.
I've reported all the bugs to SAP via their online form. The coordinated disclosure process went smoothly and I appreciate the communication I had with SAP: they took the bugs seriously, had them investigated by their team, were quick to reply and also kept me up to date without having to ping them myself. One of my better experiences disclosing bugs to a vendor by a mile.
The bug I presented in this blog post was assigned CVE-2025-42940. While I initially thought it could lead to RCE, I could not work out a working exploit, and SAP decided to rate it as a Denial-of-Service vulnerability with a CVSS score of 7.5. They also released an SAP Security Note: 3633049.
If this series has a takeaway, it's that a piece of software may be complicated, but the process for finding bugs isn't: find a thread, pull on it, and keep pulling.
If someone wants the next thread, the HDB protocol PDF is sitting right there. Either for yourself, or to feed it to your LLM agent to run overnight. As for myself, I should probably stop here before everyone starts thinking that I am the SAP person, or that I only do SAP now.
About the Author
Tao Sauvage is Director of Research at Anvil Secure. He loves finding vulnerabilities in anything he gets his hands on, especially when it involves embedded systems, reverse engineering and code review.
His previous research projects covered mobile OS security, resulting in multiple CVEs for Android, and wind farm equipment, with the creation of a proof-of-concept โwormโ targeting Antaira systems.
He used to be a core developer of the OWASP OWTF project, an offensive web testing framework, and maintain CANToolz, a python framework for black-box CAN bus analysis.
