How I get N26 API and automate a transfer

Fabrizio Waldner
11 min readJun 21, 2019

Introduction

Nowadays computer science is very advanced; and I expect to be able to simplify my life by automating and integrating several components.

For example, have you ever thought about keeping track of your tenants’ payments and sending an email to those who forgot to pay the rent?

I found N26 (n26.com) very flexible in these terms. Exploiting the API, you can create your own logic to handle your account.

You can find some unofficial API documentation on the internet, but what I didn’t find was how to make a bank transfer. Actually I did found some specification about transfers, but they are not up to date, so not working.

So I started to do reverse engineering to reveal the API, which, in my job as a system administrator, I use to do reverse engineering when there is a lack of documentation.

Disclaimer

Before publishing this article I asked the N26 security office. They gave me the permission to publish it even it is not mandatory. It was out of courtesy, because I know that the security is a delicate matter. I want thank them because they gave their point of view and I was able to understand better why some mechanisms are implemented.

I have put in quotes their comments/explanations.

Update on 5th October 2019

Since mid-September 2019, N26 has enforced the authentication method utilizing the Multi Factor Authentication. Currently, the script that I provided at the end of this story is not working anymore.

The path to the secret

Starting point

I started googling for N26 API. One of the first results is the “N26 Bug Bounty Program” (https://n26.com/en-eu/bug-bounty-program). On this page N26 give some advice on finding bugs or security problems on its infrastructure. It mentions the endpoints and the existence of some unofficial documentation of the API.

The entrypoint is api.tech26.de.

I pointed my browser to my.n26.com (where N26 said that the API are used); this host redirected me to app.n26.com/login.
Unfortunately, using the dev console of Firefox, I cannot see any query towards the api api.tech26.de. Mmmh, probably the information on the bug bounty program is not up to date.

The security office promised me to update this information on their bug bounty program page.

I started to look at what other people had already found. For example in Github project of PierrickP (https://github.com/PierrickP/n26) or the project of Zilverline (https://github.com/zilverline/ex_n26) where there is an example to retrieve the access token.

So I started to use some calls to interact with API:

curl -k -H "Authorization:Basic bXktdHJ1c3RlZC13ZHBDbGllbnQ6c2VjcmV0" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=password&username=myemail&password=mypassword" -X POST https://api.tech26.de/oauth/token

I played with some calls that I found in that repository and, finally, I tried to do a transfer with this call:

curl -H "Authorization:Bearer mytoken" -d '{"pin": "myPIN", "transaction": { "partnerIban": "myotherIBAN", "partnerBic": "BICofBank", "partnerAmount": "100", "partnerName": "myName", "referenceText": "my test transfer", "type": "DT" }}'  -H "Content-Type: application/json" -X POST https://api.tech26.de/api/transactions
Error: Update your App

Mmmh, “update your App”? This statement tells me 2 things:
- the founded API are old
- I can search for other information in the App

MITM and smartphone

So I have to sniff the communications between the N26 App and the server api.tech26.de: time for a Man In The Middle (en.wikipedia.org/wiki/Man-in-the-middle_attack).

I started to create my own CA and then sign a certificate that has the CN=api.tech26.de.

# create  the CA key
openssl genrsa -des3 -out myCA.key 2048
# Create the CA certificate
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.pem
# Create the key for the certificate
openssl genrsa -out api.tech26.de.key 2048
# Create certificate request
openssl req -new -config api.tech26.de.cnf -key api.tech26.de.key -out api.tech26.de.csr
# Use the CA to sign the request
openssl x509 -req -in api.tech26.de.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out api.tech26.de.crt -days 1825 -sha256 -extensions x509_ext -extfile api.tech26.de.ext

And set up a simple Nginx server as reverse proxy to see the traffic:

log_format postdata '[$time_local] "$request" $status '  
'$body_bytes_sent "$http_referer" '
'"$http_user_agent" [$request_body]';
server {
listen 443 ssl;
ssl_certificate certs/api.tech26.de.crt;
ssl_certificate_key certs/api.tech26.de.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
error_log stderr debug;ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass_header Server;
proxy_set_header Host $host;
proxy_pass https://128.65.211.26:443;
access_log /dev/stdout postdata;
proxy_redirect off;
}
}

I changed the resolution IP for api.tech26.de on my router, I installed my own CA on my smartphone and here we are.

The first attempt failed, it didn’t recognize the CA. I could see that on a Wireshark capture. The problem was that I used an Android 8 smartphone, because using an Android 5 smartphone, even the custom CAs are trusted.
Now, at the level of TLS session we are OK, but the App closed the connection just as the TLS handshake finished. I thought there are some restrictions on certification.

App deobfuscating and tampering

I started on documenting about security of N26, and I found this very interesting research of Dominik Maier (https://www.researchgate.net/publication/322591347_Paying_the_Price_for_Disruption_How_a_FinTech_Allowed_Account_Takeover). I read it all and it speaks about “certificate pinning”, it was missing and now they inserted it in.

Certificate pinning consists of inserting a reference about some certificates that the App has to trust. To circumvent this security I have to touch the App.

There are a lot of tool to decompile and recompile an Android App.
I used 2:

From the deobfuscated code I found that N26 uses OkHttp library and this library implements certificate pinning (https://square.github.io/okhttp/3.x/okhttp/okhttp3/CertificatePinner.html). So the research continued. I found an article of Rick Ramgattie (https://blog.securityevaluators.com/bypassing-okhttp3-certificate-pinning-c68a872ca9c8) about bypass certificate pinning.

# decompile the APK
apktool -r d extracted/base.apk -o base_to_patch
# find where fingerprints are used
grep sha256 -R base_to_patch/
./smali/com/n26/base/e/c/p.smali: const-string v3, "sha256/UFZ3yMGKM7egmNZTeK1gc5Sz/n1K/3GfWtK1RsIHDdY="
./smali/com/n26/base/e/c/p.smali: const-string v3, "sha256/uDmpTbrFp0OubCwvUNAjlvK4nCLkFZWzCa5xxNpmC3c="
./smali/com/n26/base/e/c/p.smali: const-string v3, "sha256/UFZ3yMGKM7egmNZTeK1gc5Sz/n1K/3GfWtK1RsIHDdY="
./smali/com/n26/base/e/c/p.smali: const-string v3, "sha256/WijnnlKgNnTQfDDI3TGzo9Vy6ERX/yP02FyL5iBM4Bc="
./smali/com/n26/base/e/c/p.smali: const-string v3, "sha256/CMpp+jeqJre03CLCWQTRvC6nsB6eSYpz7xCJzRRlm44="
./smali/com/n26/base/e/c/p.smali: const-string v3, "sha256/pk4REvQs+gL1agHkgWfcAEWpe6BGwJZLj50NjQ8C65Y="
# Compute the fingerprint of my own certificate
openssl x509 -in ../api.tech26.de.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
# change an hash inserting my hash
vi ./smali/com/n26/base/e/c/p.smali
# recompile
apktool b base_to_patch/ -o base_patched.apk
# sign the apk
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore base_patched.apk alias_name
# install on smartphone
adb install base_patched.apk

And here we are, this is the log of my Nginx:

INFO  ==> ** Starting NGINX **
[06/Jun/2019:10:20:51 +0000] "GET /api/version/mobile?os=android HTTP/1.1" 200 499 "-" "n26-android_9.99.9" [-]
[06/Jun/2019:10:20:56 +0000] "POST /oauth/token HTTP/1.1" 200 186 "-" "n26-android_9.99.9" [username=myemail&password=mypassword&grant_type=password]
[06/Jun/2019:10:20:56 +0000] "GET /api/smrt/categories HTTP/1.1" 200 89477 "-" "n26-android_9.99.9" [-]
[06/Jun/2019:10:20:58 +0000] "PUT /api/notificator/devices HTTP/1.1" 200 0 "-" "n26-android_9.99.9" [{\x22platform\x22:\x22ANDROID\x22,\x22publicKey\x22:\x22*****\x22,\x22token\x22:\x22*******-\x22}]
[06/Jun/2019:10:20:58 +0000] "GET /api/products HTTP/1.1" 200 2021 "-" "n26-android_9.99.9" [-]
[06/Jun/2019:10:20:59 +0000] "GET /api/me?full=true HTTP/1.1" 200 2291 "-" "n26-android_9.99.9" [-]
[06/Jun/2019:10:21:00 +0000] "GET /api/v2/cards HTTP/1.1" 200 394 "-" "n26-android_9.99.9" [-]
[06/Jun/2019:10:21:01 +0000] "GET /api/v2/translations/android+strings,credit,Savings,Overdraft,Signup+Mobile,Mobile,InsuranceWallet,KYC,Spaces,Google+Pay,Salesforce+Chat,Certification,Transactions,Feed/it HTTP/1.1" 200 86049 "-" "n26-android_9.99.9" [-]

Spying the communications

Ok, so with my reverse proxy Nginx I can see the posted data, but I cannot see the headers.
Then, I turned to use mitmdump (mitmproxy.org), a tool specifically designed to do MITM sniffing.

The syntax for doing a reverse proxy tracing the headers and body is:

sudo mitmdump -vvv  --mode reverse:https://128.65.211.26 -p443 --ssl-insecure --setheader :~q:Host:api.tech26.de --certs certs/api.tech26.de.key -w /tmp/dump.proxy  >> /tmp/log.proxy

The great thing about mitmdump is that you can save the conversation between client and server on a file (dump.proxy). This file can then be used to simulate a connection to the real server. Moreover, mitmdump allows us to substitute headers or body with a regex.

I collected some calls, but specifically I collected the dialogue for a transfer:

192.168.4.129:33141: POST https://128.65.211.26/oauth/token
routing: dee1fd7fabcdefabce4799a7b76abdac8a7a54ab3b661142e
User-Agent: n26-android_9.99.9
Authorization: Basic bmF0aXZlYW5kcm9pZDo=
Content-Type: application/x-www-form-urlencoded
Content-Length: 74
Connection: Keep-Alive
Accept-Encoding: gzip
Host: api.tech26.de
username: *******
password: ********
grant_type: password
<< 200 OK 177b
Date: Mon, 03 Jun 2019 13:14:12 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: nginx
Vary: Accept-Encoding
cache-control: no-store
x-xss-protection: 1; mode=block
pragma: no-cache
x-frame-options: DENY
server-timing: intid;desc=e897e1947eee6215
x-content-type-options: nosniff
x-envoy-upstream-service-time: 96
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Encoding: gzip
{
"access_token": "*****************",
"expires_in": 1799,
"host_url": "https://api.tech26.de",
"refresh_token": "***********",
"scope": "trust",
"token_type": "bearer"
}
192.168.4.129:55361: GET https://128.65.211.26/api/encryption/key
User-Agent: n26-android_9.99.9
Authorization: Bearer **************
Connection: Keep-Alive
Accept-Encoding: gzip
If-Modified-Since: Sun, 02 Jun 2019 19:10:28 -0000
Host: api.tech26.de
<< 200 OK 367b
Date: Mon, 03 Jun 2019 13:14:42 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: nginx
Vary: Accept-Encoding
x-envoy-upstream-service-time: 10
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Encoding: gzip
{
"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApos8rWCF1nE88M2QdxeZuGdSke+9vXPZw0Qo1iQ+X78oRBwwOa5ILrhpoG2DBwsR+aYVIFb2KHelvIvuL+UOHSaY53al2UM3cONOx7IE
ohCrBcsWpIkVdKTe29AV50L2fV391EPR0R3wHXVXf9qQR9hGZsAqZ65SWn/bTChvHcL5QoQBoU/jUdkJIxMb3ktRMfCmv+oE1oKIS/cIPGvlEw+qhfkbh+On177thrgoe2DeyxkyvU7d1j7yBBYyxJItVU88TRdmyFKXtL3pp5
DuUri0oL7W2uOBqCxhSoSLizKJ4ovERf1YerCMU8ZI5fwqPCjXK5TIMYCSXoEP7WEQywIDAQAB"
}
192.168.4.129:55361: POST https://128.65.211.26/api/transactions
Encrypted-Pin: ***base64_of_16_bytes***
Encrypted-Secret: ****base64_of_256_bytes*****
User-Agent: n26-android_9.99.9
Authorization: Bearer ********************************
Content-Type: application/json; charset=UTF-8
Content-Length: 185
Connection: Keep-Alive
Accept-Encoding: gzip
Host: api.tech26.de
{
"pin": "****",
"transaction": {
"amount": "25.0",
"partnerBic": "*****BIC***",
"partnerIban": "****IBAN*****",
"partnerName": "*****NAME****",
"referenceText": "*******",
"type": "DT"
}
}

The dialogue can be summarized in:

  • Get the bearer token: POST ouath/token
  • Get the public key to encrypt some data: GET api/encryption/key
  • Put in the transfer data and some encrypted data to validate the transfer: POST api/transactions

Decrypting the mechanism

Now, my attention is focused on how I can validate my transfer, and Encrypted-Secret and Encrypted-Pin headers are the key.

I analyzed the public key passed with GET api/encryption/key step and I was able to see that it is a public RSA of 256 bytes (2048 bit) extracted from a 2048 bit private key.

There is a rule of thumb in cryptography — the encrypted data are multiple of the length of the key used to crypt. So, probably the Encrypted-Secret header is encrypted with the public key passed.

It is almost impossible to decrypt without the private key, but I can use my own key pair. In this case I used mitmdump to substitute the public key with one of my own.

# Generate private key
openssl genrsa -des3 -out myprivate.key 2048
# Export public key
openssl rsa -in myprivate.key -outform PEM -pubout -out public.pem
# Start reverse proxy with substitution
mitmdump -vvv --mode reverse:https://128.65.211.26 \
-p443 --ssl-insecure --setheader :~q:Host:api.tech26.de \
--certs certs/api.tech26.de.key \
-S /tmp/dump.proxy \
--set flow_detail=3 \
--server-replay-kill-extra \
--replacements :~s:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApos8rWCF1nE88M2QdxeZuGdSke\\+9vXPZw0Qo1iQ\\+X78oRBwwOa5ILrhpoG2DBwsR\\+aYVIFb2KHelvIvuL\\+UOHSaY53al2UM3cONOx7IEohCrBcsWpIkVdKTe29AV50L2fV391EPR0R3wHXVXf9qQR9hGZsAqZ65SWn/bTChvHcL5QoQBoU/jUdkJIxMb3ktRMfCmv\\+oE1oKIS/cIPGvlEw\\+qhfkbh\\+On177thrgoe2DeyxkyvU7d1j7yBBYyxJItVU88TRdmyFKXtL3pp5DuUri0oL7W2uOBqCxhSoSLizKJ4ovERf1YerCMU8ZI5fwqPCjXK5TIMYCSXoEP7WEQywIDAQAB:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ttFhH4WfXGCvI41enBUeOOpwGhxCkzhibWkVgnkubZcgQsl/SeJdnLNmVZEuuQoB0cYr63FbMI7whvGoaGCryClaY2zaPlsmnaRwvzhc5dg12J7x9O9I95Vg3UdfcSfWbicUCPHneM+FEoJs6rdW98GmitWdSMdVH1IQoNtCbD2Q4v+ShxU8xGiad2uTCh7xtfhxi0H7p0O1gRd3KeeuLRJ0g8Np+3mSoqgdYRohpXq0iKGoc9eRn1mEZL49eB2oQ3RMEC6E0nQv6R2xNmvym0PEfwXb3lylu2K7RwbVGExGkqkLkmO7Qh9hk9jDAq42YoBhya7a9dQ/nU7AXsObQIDAQAB >> /tmp/log.proxy

With this command it reads the dump /tmp/dump.proxy to simulate the dialogue; it avoids asking the server with --server-replay-kill-extra and replaces the answer from the server with --replacements :~s:escaped_N26_public_key:my_public_key .

Ok, so I collected some Encrypted-Secret and Encrypted-Pin encrypted with my own key.

As said before, the public key and the Encrypted-Secret have the same length, so I tried to decrypt with RSA algorithm.

# decode from base64
base64 -d Encrypted-Secret.orig > Encrypted-Secret.nobase
# Decrypt with my own private key
openssl rsautl -decrypt -in Encrypted-Secret.nobase -inkey ./myprivate.key
{"iv":[-77,12,4,-123,76,-39,-45,90,66,-60,-77,-121,98,-123,103,-10],"secretKey":[-3,56,-24,124,68,-41,-79,32,61,-106,83,-33,120,55,-5,109]}

Wow! I obtained two arrays: iv and secret. Be careful, they are 16 bytes length, the value of single integer is between -128 and 127, so they are signed bytes. I am not a security researcher, but my experience told me that iv stand for initialization vector: it is normally used in CBC encryption.
At this moment, I was not sure how use this arrays, so I started to dig inside the deobfuscated sources. In the code there is a lot of code about encryption because they come from the library Bouncycastle, so? I focused my attention on the code of developers, in particular I found this class:

package com.n26.base.security.p123a;import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import kotlin.p820e.p821b.C14870k;
/* compiled from: AesEncrypter.kt */
/* renamed from: com.n26.base.security.a.c */
public final class C1635c {
/* renamed from: a */
public final byte[] m4919a(byte[] bArr, Key key, byte[] bArr2) {
C14870k.b(bArr, "bytesToEncrypt");
C14870k.b(key, "secretKey");
C14870k.b(bArr2, "initializationVector");
try {
Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding");
instance.init(1, key, new IvParameterSpec(bArr2));
bArr = instance.doFinal(bArr);
C14870k.a(bArr, "cipher.doFinal(bytesToEncrypt)");
return bArr;
} catch (byte[] bArr3) {
throw ((Throwable) new IllegalArgumentException("Exception raised during AES encryption", (Throwable) bArr3));
}
}
}

I found a useful site where I can decrypt AES-128 (=16*8bytes): https://cryptii.com/pipes/aes-encryption

To fill the forms of the site page, I have to convert to hex, so I scripted:

echo ENCODED=
base64 -d Encrypted-Pin.orig | hexdump -C
echo IV=
arr="$( echo -77,12,4,-123,76,-39,-45,90,66,-60,-77,-121,98,-123,103,-10 | tr ',' '\n')"
for num in "${arr[@]}" ; do printf "%02x\n" $num | rev | cut -c 1-2 | rev; done
echo SECRET=
arr="$( echo -3,56,-24,124,68,-41,-79,32,61,-106,83,-33,120,55,-5,109 | tr ',' '\n')"
for num in "${arr[@]}" ; do printf "%02x\n" $num | rev | cut -c 1-2 | rev; done
======================== Cleaned Output =======================
ENCODED= ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
IV= b3 0c 04 85 4c d9 d3 5a 42 c4 b3 87 62 85 67 f6
SECRET= fd 38 e8 7c 44 d7 b1 20 3d 96 53 df 78 37 fb 6d

And I inserted in the online decrypter:

Voilà, the decrypted content is my PIN, the PIN that I have to use to confirm the transaction. The same PIN is passed in plain text in the body, but without the Encrypted-Pin header, the transfer is not accepted.

The security team told me that the plain PIN will disappear in the next versions of the API, now they are experiencing the transient to be retro-compatible.

Now, I can probably make a transfer. To do it, I generated an arbitrary and simple encryption of my PIN:

And I tried with some curl, it works, the iv and secret are arbitrary.

Scripted transfer

Schema of the encryption

Finally, I can translate everything together in a script:

pin=****
email=************
password=***************
IBAN=****************
BIC=*****
amount="25.0"
motivation="Scripted transfer"
recipient="Fabrizio Waldner"
###########
# Get TOKEN
respToken=$(curl -k -H "Authorization:Basic bmF0aXZlYW5kcm9pZDo=" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=password&username=$email&password=$password" -X POST https://api.tech26.de/oauth/token)
token=$(echo $respToken | jq -r '.access_token')
####################
# GET PUBLIC N26 KEY
respKey=$(curl -k -H "Authorization:Bearer $token" https://api.tech26.de/api/encryption/key)
publicKey=$(echo $respKey | jq -r '.publicKey')echo "-----BEGIN PUBLIC KEY-----" > /tmp/pubkey
echo "$publicKey" >> /tmp/pubkey
echo "-----END PUBLIC KEY-----" >> /tmp/pubkey
#################
# COMPUTE HEADERS
encryptedSecret=$(echo '{"iv":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"secretKey":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]}' | openssl rsautl -encrypt -inkey /tmp/pubkey -pubin | base64 -w0)
encryptedPin=$(echo -n $pin | openssl enc -aes-128-cbc -K 000102030405060708090a0b0c0d0e0f -iv 000102030405060708090a0b0c0d0e0f | base64 -w0)
#############
# DO TRANSFER
curl -k -H "Authorization:Bearer $token" \
-d "{\"pin\": \"$pin\", \"transaction\": { \"partnerIban\": \"$IBAN\", \"partnerBic\": \"$BIC\", \"amount\": \"$amount\", \"partnerName\": \"$recipient\", \"referenceText\": \"$motivation\", \"type\": \"DT\" }}" \
-H "Content-Type: application/json; charset=UTF-8" \
-H "Encrypted-Pin: $encryptedPin" \
-H "Encrypted-Secret: $encryptedSecret" \
-X POST https://api.tech26.de/api/transactions

Conclusion

I found it very strange that the process crypts the PIN with a secret (encrypted) that is given together. I don’t think this adds security, the security is provided by the asymmetrical encryption with the public key. It looks like “security through obscurity” paradigm, a paradigm that normally one should avoid.

The security team explained to me that in reality this mechanism adds more security. Actually, crypting a small amount of data (4 digits of the PIN) with the public key can lead to a cryptanalysis attack. To add entropy they introduced secret with iv. From this point of view I can agree with them.

This article doesn’t show security problems, but it aims to be a help for those who want to start the bug bounty program. I think security is sharing the knowledge and open sourcing the mechanism.

But most of all, now I can make a transfer in automated way with the above script.

--

--

Fabrizio Waldner

Site Reliability Engineer at Google. This is my personal blog, thoughts and opinions belong solely to me and not to my employer.