How I get N26 API and automate a transfer

Introduction

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

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.

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.

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
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

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).

# 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
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;
}
}

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.

# 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
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.

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
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"
}
}
  • 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.

# 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
# 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]}
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));
}
}
}
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

Scripted transfer

Schema of the encryption
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.

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