├── Jodel-Keyhack-v3
├── frontend-src
│ ├── assets
│ │ └── .gitkeep
│ ├── styles.less
│ ├── environments
│ │ ├── environment.prod.ts
│ │ ├── environment.ts
│ │ └── environment.hmr.ts
│ ├── favicon.ico
│ ├── proxy.conf.json
│ ├── app
│ │ ├── app.component.less
│ │ ├── components
│ │ │ ├── copy-input.component.html
│ │ │ ├── copy-input.component.ts
│ │ │ └── copy-input.component.less
│ │ ├── app.module.ts
│ │ ├── app.component.ts
│ │ ├── app.component.spec.ts
│ │ └── app.component.html
│ ├── tsconfig.app.json
│ ├── .editorconfig
│ ├── tsconfig.spec.json
│ ├── index.html
│ ├── browserslist
│ ├── tslint.json
│ ├── hmr.ts
│ ├── test.ts
│ ├── main.ts
│ ├── karma.conf.js
│ └── polyfills.ts
├── .vscode
│ ├── settings.json
│ └── tasks.json
├── tsconfig.json
├── backend
│ ├── test
│ │ ├── hmac_test.py
│ │ └── api_test.py
│ ├── decrypt.py
│ ├── r2instance.py
│ └── server.py
├── requirements.txt
├── README.md
├── package.json
├── .gitignore
├── tslint.json
└── angular.json
├── Jodel-Keyhack-Frida
├── extract_hmac_ios.js
├── extract_hmac.js
├── Readme.md
└── extract_hmac.py
├── README.md
├── Jodel-Keyhack-v2
├── jodel_ios.py
├── decrypt.py
├── jodel.py
└── README.md
├── 03_Reversing_SSL-Pinning.md
├── 01_Reversing_App.md
├── 02_Reversing_HMAC.md
└── 04_Reversing_Android_Email_Verification.md
/Jodel-Keyhack-v3/frontend-src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.venvPath": "venv",
3 | "python.linting.enabled": false
4 | }
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/styles.less:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | hmr: false
4 | };
5 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JodelRaccoons/JodelReversing/HEAD/Jodel-Keyhack-v3/frontend-src/favicon.ico
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://127.0.0.1:5000/",
4 | "secure": false
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/app.component.less:
--------------------------------------------------------------------------------
1 | .container {
2 | height: 100%;
3 | display: flex;
4 | align-items: center;
5 | width: 550px;
6 | margin: auto;
7 | justify-content: center;
8 | }
9 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "types": ["node"]
6 | },
7 | "exclude": [
8 | "test.ts",
9 | "**/*.spec.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "test.ts",
12 | "polyfills.ts"
13 | ],
14 | "include": [
15 | "**/*.spec.ts",
16 | "**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-Frida/extract_hmac_ios.js:
--------------------------------------------------------------------------------
1 | Interceptor.attach(ObjC.classes.JDLAPIRequestHMACHashBuilder['- secretKey'].implementation, {
2 | onLeave: function (retval) {
3 | console.log('[+] HMAC-Key: ', new ObjC.Object(ptr(retval)).toString());
4 | console.log('[+] Version: ' + ObjC.classes.NSBundle.mainBundle().objectForInfoDictionaryKey_("CFBundleShortVersionString").UTF8String())
5 | },
6 | });
7 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | R2 HMAC Extractor
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/browserslist:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | #
5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
6 |
7 | last 2 Chrome versions
8 | not IE 9-11
9 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "app",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "app",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/components/copy-input.component.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/components/copy-input.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, ViewEncapsulation } from "@angular/core";
2 |
3 | @Component({
4 | selector: "app-copy-input",
5 | templateUrl: "./copy-input.component.html",
6 | styleUrls: ["./copy-input.component.less"],
7 | encapsulation: ViewEncapsulation.None
8 | })
9 | export class CopyInputComponent {
10 | @Input() value = "";
11 |
12 | async copy(value) {
13 | await (navigator as any).clipboard.writeText(value);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "module": "es2015",
9 | "moduleResolution": "node",
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "importHelpers": true,
13 | "target": "es5",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2018",
19 | "dom"
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/hmr.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationRef, NgModuleRef } from "@angular/core";
2 | import { createNewHosts } from "@angularclass/hmr";
3 |
4 | export const hmrBootstrap = (
5 | module: any,
6 | bootstrap: () => Promise>
7 | ) => {
8 | let ngModule: NgModuleRef;
9 | module.hot.accept();
10 | bootstrap().then(mod => (ngModule = mod));
11 | module.hot.dispose(() => {
12 | const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
13 | const elements = appRef.components.map(c => c.location.nativeElement);
14 | const makeVisible = createNewHosts(elements);
15 | ngModule.destroy();
16 | makeVisible();
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/backend/test/hmac_test.py:
--------------------------------------------------------------------------------
1 | import jodel_api
2 |
3 | if __name__ == '__main__':
4 |
5 | "a4a8d4d7b09736a0f65596a868cc6fd620920fb0" \
6 | "PUT@/api/v2/posts/5d239cac23e3e1001bfbb1cf/upvote, PUT" \
7 | "%" \
8 | "api.go-tellm.com" \
9 | "%" \
10 | "443" \
11 | "%" \
12 | "/api/v2/posts/5d239cac23e3e1001bfbb1cf/upvote" \
13 | "%" \
14 | "12259522-a84741ac-a4e89ecc-acdc-4aba-aaf5-4e51572ea674" \
15 | "%" \
16 | "49.8831;8.6690" \
17 | "%" \
18 | "2019-07-08T19:44:29Z" \
19 | "%" \
20 | "home" \
21 | "%" \
22 | "false" \
23 | "%" \
24 | "{}" \
25 | "Returned: 463582933e3b5a1e0a1a9cf89ea8842924f2701f"
26 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/components/copy-input.component.less:
--------------------------------------------------------------------------------
1 | @import "~ng-zorro-antd/input/style/mixin.less";
2 | .custom-copy-input {
3 | input {
4 | border-right: 0;
5 | &:hover,
6 | &:focus {
7 | border-color: @input-border-color;
8 | box-shadow: none;
9 | }
10 | }
11 |
12 | .ant-input-group-addon {
13 | transition: all 0.05s;
14 | padding: 0;
15 | line-height: 2;
16 | &:last-child {
17 | border-left: 1px solid @input-border-color;
18 | }
19 | &:hover {
20 | .hover;
21 | }
22 | &:active {
23 | .active;
24 | }
25 |
26 | .button-ify {
27 | padding: 0 11px;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/requirements.txt:
--------------------------------------------------------------------------------
1 | -e git+https://github.com/krokofant/apk-signature-verify.git@215b9a86979eb8e2a9ff97b7a8758e70ee361345#egg=apkverify
2 | asn1crypto==0.24.0
3 | autopep8==1.4.3
4 | certifi==2018.11.29
5 | chardet==3.0.4
6 | Click==7.0
7 | Flask==1.0.2
8 | future==0.17.1
9 | idna==2.8
10 | itsdangerous==1.1.0
11 | Jinja2==2.11.3
12 | -e git+https://github.com/Unbrick/jodel_api.git@be12ddcabd59b9fcd9ad08c1ff703a05b1d08b00#egg=jodel_api
13 | lxml==4.9.1
14 | MarkupSafe==1.1.0
15 | mock==2.0.0
16 | pbr==5.1.1
17 | protobuf==3.15.0
18 | pyaxmlparser==0.3.13
19 | pycodestyle==2.4.0
20 | r2pipe==1.2.0
21 | requests==2.21.0
22 | six==1.12.0
23 | urllib3==1.26.5
24 | varint==1.0.2
25 | Werkzeug==0.15.3
26 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | hmr: false
8 | };
9 |
10 | /*
11 | * For easier debugging in development mode, you can import the following file
12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
13 | *
14 | * This import should be commented out in production mode because it will have a negative impact
15 | * on performance if an error is thrown.
16 | */
17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
18 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/environments/environment.hmr.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | hmr: true
8 | };
9 |
10 | /*
11 | * For easier debugging in development mode, you can import the following file
12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
13 | *
14 | * This import should be commented out in production mode because it will have a negative impact
15 | * on performance if an error is thrown.
16 | */
17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
18 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from "@angular/core";
2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
3 | import { AppModule } from "./app/app.module";
4 | import { environment } from "./environments/environment";
5 | import { hmrBootstrap } from "./hmr";
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule);
12 |
13 | if (environment.hmr) {
14 | if (module["hot"]) {
15 | hmrBootstrap(module, bootstrap);
16 | } else {
17 | console.error("HMR is not enabled for webpack-dev-server!");
18 | console.log("Are you using the --hmr flag for ng serve?");
19 | }
20 | } else {
21 | bootstrap().catch(err => console.log(err));
22 | }
23 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { registerLocaleData } from "@angular/common";
2 | import { HttpClientModule } from "@angular/common/http";
3 | import en from "@angular/common/locales/en";
4 | import { NgModule } from "@angular/core";
5 | import { FormsModule } from "@angular/forms";
6 | import { BrowserModule } from "@angular/platform-browser";
7 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
8 | import { en_US, NgZorroAntdModule, NZ_I18N } from "ng-zorro-antd";
9 | import { AppComponent } from "./app.component";
10 | import { CopyInputComponent } from "./components/copy-input.component";
11 |
12 | registerLocaleData(en);
13 |
14 | @NgModule({
15 | declarations: [AppComponent, CopyInputComponent],
16 | imports: [
17 | BrowserModule,
18 | NgZorroAntdModule,
19 | FormsModule,
20 | HttpClientModule,
21 | BrowserAnimationsModule
22 | ],
23 | providers: [{ provide: NZ_I18N, useValue: en_US }],
24 | bootstrap: [AppComponent]
25 | })
26 | export class AppModule {}
27 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../coverage'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false
30 | });
31 | };
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "windows": {
6 | "options": {
7 | "shell": {
8 | "executable": "powershell.exe"
9 | }
10 | }
11 | },
12 | "tasks": [
13 | {
14 | "label": "Create virtualenv",
15 | "type": "shell",
16 | "windows": {
17 | "command": "py -3 -m venv venv"
18 | },
19 | "osx": {
20 | "command": "python3 -m venv venv"
21 | },
22 | "presentation": {
23 | "echo": false
24 | },
25 | "problemMatcher": []
26 | },
27 | {
28 | "label": "Activate virtualenv",
29 | "type": "shell",
30 | "windows": {
31 | "command": "venv\\Scripts\\activate",
32 | "options": { "shell": { "args": ["-NoExit"] } }
33 | },
34 | "osx": {
35 | "command": ". venv/bin/activate"
36 | },
37 | "presentation": {
38 | "echo": true,
39 | "panel": "new"
40 | },
41 | "problemMatcher": []
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "@angular/core";
2 | import { UploadChangeParam, UploadFile } from "ng-zorro-antd";
3 |
4 | export interface UploadResponse {
5 | package: string;
6 | version_name: string;
7 | version_code: string;
8 | signature_verified: boolean;
9 | is_jodel_signature: boolean;
10 | certs: string;
11 | hmac_key: string;
12 | key_status: { working: boolean };
13 | error: false;
14 | message: string;
15 | }
16 |
17 | @Component({
18 | selector: "app-root",
19 | templateUrl: "./app.component.html",
20 | styleUrls: ["./app.component.less"]
21 | })
22 | export class AppComponent {
23 | successfulExtraction = false;
24 | extractionData: UploadResponse;
25 | fileList: UploadFile[] = [];
26 |
27 | fileChange(uploadChangeParam: UploadChangeParam) {
28 | const { type, file, fileList } = uploadChangeParam;
29 | if (type === "success") {
30 | this.extractionData = file.response;
31 | this.successfulExtraction = true;
32 | } else {
33 | this.successfulExtraction = false;
34 | this.fileList = fileList.slice(-1);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { AppComponent } from './app.component';
3 |
4 | describe('AppComponent', () => {
5 | beforeEach(async(() => {
6 | TestBed.configureTestingModule({
7 | declarations: [
8 | AppComponent
9 | ],
10 | }).compileComponents();
11 | }));
12 |
13 | it('should create the app', () => {
14 | const fixture = TestBed.createComponent(AppComponent);
15 | const app = fixture.debugElement.componentInstance;
16 | expect(app).toBeTruthy();
17 | });
18 |
19 | it(`should have as title 'my-project'`, () => {
20 | const fixture = TestBed.createComponent(AppComponent);
21 | const app = fixture.debugElement.componentInstance;
22 | expect(app.title).toEqual('my-project');
23 | });
24 |
25 | it('should render title in a h1 tag', () => {
26 | const fixture = TestBed.createComponent(AppComponent);
27 | fixture.detectChanges();
28 | const compiled = fixture.debugElement.nativeElement;
29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to my-project!');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/README.md:
--------------------------------------------------------------------------------
1 | # Jodel Keyhack v3 (now with fancy angular gui)
2 |
3 | # Install on Windows
4 |
5 | (for Windows) Install package manager [Chocolatey](https://chocolatey.org/) for programs and
6 | [scoop](https://scoop.sh/) for dev tools
7 |
8 | 1. Install requirements
9 | - `scoop install radare2`
10 | - `choco install nodejs`
11 | - `choco install python3`
12 | 2. Create virtual env
13 | - `py -3 -m venv venv`
14 | 3. Activate virtualenv
15 | - `venv\Scripts\activate`
16 | 4. Install python deps
17 | - `pip install -r requirements.txt`
18 | 5. Build frontend
19 | - `npm ci`
20 | - `npm run build:prod`
21 | 6. Start backend
22 | - `python3 backend/server.py`
23 |
24 | # Install on macOS
25 |
26 | 1. Install requirements
27 | - `brew install radare2`
28 | - `brew install nodejs`
29 | - `brew install python3`
30 | 2. Create virtual env
31 | - `python3 -m venv venv`
32 | 3. Activate virtualenv
33 | - `. venv/bin/activate`
34 | 4. Install python deps
35 | - `pip install -r requirements.txt`
36 | 5. Build frontend
37 | - `npm ci`
38 | - `npm run build:prod`
39 | 6. Start backend
40 | - `python3 backend/server.py`
41 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/backend/test/api_test.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 |
3 | import jodel_api
4 | import hmac, hashlib
5 |
6 | from jodel_api import gcmhack
7 |
8 | if __name__ == '__main__':
9 | lat, lng, city = 48.148434, 11.567867, "Munich"
10 | account = gcmhack.AndroidAccount()
11 | sleep(5)
12 | token = account.get_push_token()
13 | j = jodel_api.JodelAccount(lat=lat, lng=lng, city=city, pushtoken=token)
14 | print('Account created')
15 | print('Verification: {}'.format(j.verify(android_account=account)))
16 | print(j.get_posts_popular())
17 | print('Karma is: {}'.format(j.get_karma()))
18 | print(j.upvote('5d238be544a6a0001a0360d6'))
19 | print('Karma is: {}'.format(j.get_karma()))
20 |
21 | #req = 'POST%api.go-tellm.com%443%/api/v2/users/%%%2019-01-10T21:11:59Z%%{"location":{"country":"DE","city":"Heilbronn","loc_coordinates":{"lng":9.2070918,"lat":49.1208046},"loc_accuracy":16.581},"registration_data":{"channel":"","provider":"branch.io","campaign":"","feature":"","referrer_branch_id":"","referrer_id":""},"client_id":"81e8a76e-1e02-4d17-9ba0-8a7020261b26","device_uid":"573b2e648c7b3849f1a533167354f4752f0466010c72bdde378a5d856421b122","language":"de-DE"}'
22 |
23 | #signature = hmac.new('TNHfHCaBjTvtrjEFsAFQyrHapTHdKbJVcraxnTzd'.encode("utf-8"), req.encode("utf-8"), hashlib.sha1).hexdigest().upper()
24 | #print(signature)
25 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-project",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "hmr": "ng serve --configuration hmr",
8 | "build": "ng build",
9 | "build:watch": "ng build --watch",
10 | "build:prod": "ng build --prod",
11 | "test": "ng test",
12 | "lint": "ng lint"
13 | },
14 | "private": true,
15 | "dependencies": {
16 | "@angular/animations": "~8.1.1",
17 | "@angular/common": "~8.1.1",
18 | "@angular/compiler": "~8.1.1",
19 | "@angular/core": "~11.0.5",
20 | "@angular/forms": "~8.1.1",
21 | "@angular/platform-browser": "~8.1.1",
22 | "@angular/platform-browser-dynamic": "~8.1.1",
23 | "@angular/router": "~8.1.1",
24 | "core-js": "^3.1.4",
25 | "ng-zorro-antd": "8.0.2",
26 | "rxjs": "~6.5.2",
27 | "tslib": "^1.10.0",
28 | "zone.js": "~0.9.1"
29 | },
30 | "devDependencies": {
31 | "@angular-devkit/build-angular": "~0.801.1",
32 | "@angular/cli": "~8.1.1",
33 | "@angular/compiler-cli": "~8.1.1",
34 | "@angular/language-service": "~8.1.1",
35 | "@angularclass/hmr": "^2.1.3",
36 | "@types/jasmine": "~3.3.13",
37 | "@types/jasminewd2": "~2.0.6",
38 | "@types/node": "~12.6.2",
39 | "codelyzer": "~5.1.0",
40 | "jasmine-core": "~3.4.0",
41 | "jasmine-spec-reporter": "~4.2.1",
42 | "karma": "~6.3.16",
43 | "karma-chrome-launcher": "~3.0.0",
44 | "karma-coverage-istanbul-reporter": "~2.0.5",
45 | "karma-jasmine": "~2.0.1",
46 | "karma-jasmine-html-reporter": "^1.4.2",
47 | "protractor": "~5.4.2",
48 | "ts-node": "~8.3.0",
49 | "tslint": "~5.18.0",
50 | "typescript": "3.4.5"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-Frida/extract_hmac.js:
--------------------------------------------------------------------------------
1 | const DEBUG = false;
2 |
3 | function stringFromByteArray(bArray) {
4 | const extraByteMap = [ 1, 1, 1, 1, 2, 2, 3, 0 ];
5 | var count = bArray.length;
6 | var str = "";
7 |
8 | for (var index = 0;index < count;)
9 | {
10 | var ch = bArray[index++];
11 | if (ch & 0x80)
12 | {
13 | var extra = extraByteMap[(ch >> 3) & 0x07];
14 | if (!(ch & 0x40) || !extra || ((index + extra) > count))
15 | return null;
16 |
17 | ch = ch & (0x3F >> extra);
18 | for (;extra > 0;extra -= 1)
19 | {
20 | var chx = bArray[index++];
21 | if ((chx & 0xC0) != 0x80)
22 | return null;
23 |
24 | ch = (ch << 6) | (chx & 0x3F);
25 | }
26 | }
27 |
28 | str += String.fromCharCode(ch);
29 | }
30 | return str;
31 | }
32 |
33 | Java.perform(function () {
34 |
35 | var alreadyPrinted = false;
36 |
37 | Java.use('com.jodelapp.jodelandroidv3.JodelApp').onCreate.overload().implementation = function() {
38 | this.onCreate();
39 | console.log("\r\nVersion: " + this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName.value);
40 | }
41 |
42 | Java.use('javax.crypto.Mac').init.overload('java.security.Key').implementation = function (v) {
43 | if (!alreadyPrinted) {
44 | console.log("HMAC-Key: " + stringFromByteArray(v.getEncoded()));
45 | alreadyPrinted = true;
46 | }
47 |
48 | return this.init(v);
49 | };
50 |
51 | if (DEBUG)
52 | Java.use('javax.crypto.Mac').doFinal.overload('[B').implementation = function(toBeHmaced) {
53 | console.log("To be HMACed: " + stringFromByteArray(toBeHmaced));
54 | return this.doFinal(toBeHmaced);
55 | }
56 | });
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JodelReversing
2 | Some details about reversing the Jodel-App
3 |
4 | This repository consists of different parts.
5 |
6 | #### Jodel-Keyhack-Frida
7 | There are three different methods to extract the HMAC-Key from the Jodel APK.
8 | The most reliable method as of my tests is the [JodelKeyhack-Frida](https://github.com/JodelRaccoons/JodelReversing/tree/master/Jodel-Keyhack-Frida) but it requires a rooted Android Device with SELinux set to permissive and [frida-server](https://github.com/frida/frida/releases) installed (Android) or a Jailbroken iDevice.
9 |
10 | #### Jodel-Keyhack-v2
11 | If Jodel-Keyhack-Frida is not an option for you, you could try the Jodel-Keyhack-v2 which is based on IDA Pro and their Python scripting interface.
12 | From time to time it is a little bit unstable but generally it produces working and valid results.
13 |
14 | #### Jodel-Keyhack-v3
15 | The last option is to use the Jodel-Keyhack-v3 which is based on radare2 and includes a fancy webinterface for uploading the APK and displaying the signature.
16 | The v3 is currently not working as radare2 is not able to resolve the function names correctly. A quick and dirty hack would be searching for the correct pattern inside the functions.
17 | Althogh this is a possibility, it did not prove to be reliable. Therefore the v3 is as of now **BROKEN**.
18 |
19 | ### Information about reversing
20 | The markdown documents included in this repository sums up basics about the application and steps to reverse engenieer it.
21 | There might be some caveats and none of the things documented are guaranteed to work anymore.
22 | The algorithms used could change at any point in time.
23 |
24 | In case some things do not work anymore or parts of the documentation are not understandable, feel free to create an issue on this repository.
25 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v2/jodel_ios.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from idautils import *
3 | from idaapi import *
4 | from idc import *
5 | import sys, traceback
6 | import decrypt
7 | import re
8 |
9 | FUNCTION_PATTERN = 'convertNSStringToCString'
10 |
11 | REGEX_EXTRACT_LOCATION = r'byte_(\S*)'
12 |
13 |
14 | def locate_function():
15 | for segea in Segments():
16 | found = False
17 | for funcea in Functions(segea, idc.get_segm_end(segea)):
18 | functionName = idc.get_func_name(funcea)
19 | if FUNCTION_PATTERN in functionName:
20 | found = True
21 | print("Found function ", functionName, " at ", funcea)
22 | continue
23 | if found:
24 | return funcea
25 | return None
26 |
27 |
28 | def get_disassembly(funcea):
29 | disasm = []
30 | for (startea, endea) in Chunks(funcea):
31 | for head in Heads(startea, endea):
32 | disasm.append(GetDisasm(head))
33 | return disasm
34 |
35 |
36 | def extract_key():
37 | xor_key = "ed25b40c912702e08c2b2a06eae635e03f475cc3"
38 | target_function = locate_function()
39 | print("Found function with pattern",FUNCTION_PATTERN, "at", target_function)
40 |
41 | disasm = get_disassembly(locate_function())
42 | for asm in disasm:
43 | if 'MOV' in asm:
44 | # find offset of key
45 | raw_location = re.findall(REGEX_EXTRACT_LOCATION, asm)
46 | # get key bytes
47 | raw_bytes = idc.get_bytes(int(raw_location[0],16), 40)
48 | print("Got raw key:", raw_bytes.hex())
49 | # perform XOR on key with xor_key
50 | print("Decrypted key:", ''.join([chr(_byte ^ ord(xor_key[count])) for count, _byte in enumerate(raw_bytes)]))
51 | return
52 |
53 |
54 | if __name__ == '__main__':
55 | try:
56 | key = extract_key()
57 | except Exception as e:
58 | print('Exception: {}'.format(e))
59 | print(traceback.print_exc(file=sys.stdout))
60 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v2/decrypt.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #tm
3 | import binascii
4 |
5 | CLIENT_SECRET_SIZE = 40
6 | CRYPTTABLE_SIZE = 256
7 | SIGNATURE = "a4a8d4d7b09736a0f65596a868cc6fd620920fb0"
8 |
9 | def GenerateEncryptionKey():
10 | encryptionKey = [None] * CRYPTTABLE_SIZE
11 | sig = SIGNATURE
12 | signatureLength = len(sig)
13 |
14 | shuffleCounter = 0
15 |
16 | for i in range(CRYPTTABLE_SIZE):
17 | encryptionKey[i] = i & 0xff
18 |
19 | for shuffleIndex in range(CRYPTTABLE_SIZE):
20 | encryptionKeyByte = encryptionKey[shuffleIndex] & 0xff
21 | shuffleCounter += ord(sig[shuffleIndex % signatureLength])
22 | shuffleCounter += encryptionKeyByte
23 | shuffleCounter &= 0xff
24 |
25 | encryptionKey[shuffleIndex] = encryptionKey[shuffleCounter]
26 | encryptionKey[shuffleCounter] = encryptionKeyByte
27 |
28 | return encryptionKey
29 |
30 |
31 | def decrypt(xorKey):
32 | #xorKey = b''.join(map(lambda x: int(x, 16).to_bytes(1, 'little'), xorKey))
33 |
34 | clientSecret = [None] * (CLIENT_SECRET_SIZE+1)
35 | secretCounter = 0
36 |
37 | encryptionKey = GenerateEncryptionKey()
38 |
39 | for secretIndex in range(CLIENT_SECRET_SIZE):
40 | encryptionKeyByte = encryptionKey[secretIndex + 1] & 0xff
41 | secretCounter += encryptionKeyByte
42 | secretCounter &= 0xff
43 |
44 | encryptionKey[secretIndex + 1] = encryptionKey[secretCounter]
45 | encryptionKey[secretCounter] = encryptionKeyByte
46 | clientSecret[secretIndex] = (xorKey[secretIndex] ^ encryptionKey[(encryptionKey[secretIndex + 1] + encryptionKeyByte) & 0xff]) & 0xff
47 |
48 | s = ''
49 | for i in range(CLIENT_SECRET_SIZE):
50 | s += "%02x" % clientSecret[i]
51 |
52 | return binascii.unhexlify(s)
--------------------------------------------------------------------------------
/Jodel-Keyhack-Frida/Readme.md:
--------------------------------------------------------------------------------
1 | # Jodel Keyhack using Frida
2 |
3 | #### Requirements:
4 |
5 | - python
6 | - pip
7 | - adb
8 | - root on Android device
9 | - SELinux permissive kernel!
10 |
11 | #### Download and references
12 | [Frida for Android reference](https://www.frida.re/docs/android/)
13 | [frida-server releases](https://github.com/frida/frida/releases)
14 | - Download frida-server-[latest]-android-[arch_of_your_phone].xz and unpack
15 |
16 | #### Bring it to work:
17 | ```
18 | pip install frida frida-tools
19 | adb push frida-server-[latest]-android-[arch_of_your_phone] /data/local/tmp/
20 | adb shell
21 | cd /data/local/tmp
22 | su
23 | chmod +x frida-server-[latest]-android-[arch_of_your_phone]
24 | ./frida-server-[latest]-android-[arch_of_your_phone] &
25 |
26 | ```
27 |
28 | ### Run it
29 | To begin, start Jodel on your Android device. Afterwards start the python script:
30 |
31 | ```bash
32 | $ python extract_hmac.py
33 | Version: 7.25.8
34 | HMAC-Key: mNnKgrObHerZMdpPffBMbKvYZxNDpQYGDWzzsyjH
35 | ```
36 |
37 | Or run the javascript file directly with frida:
38 |
39 | ```bash
40 | frida -U -l extract_hmac.js -f com.tellm.android.app --no-pause
41 | ____
42 | / _ | Frida 15.0.8 - A world-class dynamic instrumentation toolkit
43 | | (_| |
44 | > _ | Commands:
45 | /_/ |_| help -> Displays the help system
46 | . . . . object? -> Display information about 'object'
47 | . . . . exit/quit -> Exit
48 | . . . .
49 | . . . . More info at https://frida.re/docs/home/
50 | Spawned `com.tellm.android.app`. Resuming main thread!
51 | [Pixel::com.tellm.android.app]->
52 | Version: 7.25.8
53 | HMAC-Key: mNnKgrObHerZMdpPffBMbKvYZxNDpQYGDWzzsyjH
54 | ```
55 |
56 | ### iOS version
57 | To also support iOS versions, a new script has been added. Run it with:
58 |
59 | ```bash
60 | $ frida -U -l extract_hmac_ios.js -n Jodel
61 | ```
62 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/backend/decrypt.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | #tm
3 | import binascii
4 |
5 | #KEY_LOCATION_START = 0xFC00
6 | #KEY_LOCATION_HAYSTACK_SIZE = 0x1000
7 | CLIENT_SECRET_SIZE = 40
8 | CRYPTTABLE_SIZE = 256
9 | SIGNATURE = "a4a8d4d7b09736a0f65596a868cc6fd620920fb0"
10 |
11 | def GenerateEncryptionKey():
12 | encryptionKey = [None] * CRYPTTABLE_SIZE
13 | sig = SIGNATURE
14 | signatureLength = len(sig)
15 |
16 | shuffleCounter = 0
17 |
18 | for i in range(CRYPTTABLE_SIZE):
19 | encryptionKey[i] = i & 0xff
20 |
21 | for shuffleIndex in range(CRYPTTABLE_SIZE):
22 | encryptionKeyByte = encryptionKey[shuffleIndex] & 0xff
23 | shuffleCounter += ord(sig[shuffleIndex % signatureLength])
24 | shuffleCounter += encryptionKeyByte
25 | shuffleCounter &= 0xff
26 |
27 | encryptionKey[shuffleIndex] = encryptionKey[shuffleCounter]
28 | encryptionKey[shuffleCounter] = encryptionKeyByte
29 |
30 | return encryptionKey
31 |
32 |
33 | def decrypt(xorKey):
34 | #xorKey = b''.join(map(lambda x: int(x, 16).to_bytes(1, 'little'), xorKey))
35 |
36 | clientSecret = [None] * (CLIENT_SECRET_SIZE+1)
37 | secretCounter = 0
38 |
39 | encryptionKey = GenerateEncryptionKey()
40 |
41 | for secretIndex in range(CLIENT_SECRET_SIZE):
42 | encryptionKeyByte = encryptionKey[secretIndex + 1] & 0xff
43 | secretCounter += encryptionKeyByte
44 | secretCounter &= 0xff
45 |
46 | encryptionKey[secretIndex + 1] = encryptionKey[secretCounter]
47 | encryptionKey[secretCounter] = encryptionKeyByte
48 | clientSecret[secretIndex] = (xorKey[secretIndex] ^ encryptionKey[(encryptionKey[secretIndex + 1] + encryptionKeyByte) & 0xff]) & 0xff
49 |
50 | s = ''
51 | for i in range(CLIENT_SECRET_SIZE):
52 | s += "%02x" % clientSecret[i]
53 |
54 | return binascii.unhexlify(s)
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 | src/
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | .hypothesis/
51 | .pytest_cache/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # IPython
79 | profile_default/
80 | ipython_config.py
81 |
82 | # pyenv
83 | .python-version
84 |
85 | # celery beat schedule file
86 | celerybeat-schedule
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | env.bak/
98 | venv.bak/
99 |
100 | # Spyder project settings
101 | .spyderproject
102 | .spyproject
103 |
104 | # Rope project settings
105 | .ropeproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 | .dmypy.json
113 | dmypy.json
114 |
115 | # Pyre type checker
116 | .pyre/
117 |
118 |
119 | # angular
120 | node_modules/
121 | frontend-dist/
122 |
123 | # macos
124 | .DS_Store
125 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 | Upload Jodel APK
13 | Just drag and drop the APK file
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Offical Jodel APK
22 |
{{ extractionData.is_jodel_signature ? "Yes" : "No" }}
23 |
24 |
25 |
26 |
Package name
27 |
{{ extractionData.package }}
28 |
29 |
30 |
Version name
31 |
{{ extractionData.version_name }}
32 |
33 |
34 |
Version code
35 |
{{ extractionData.version_code }}
36 |
37 |
43 |
44 |
Key status
45 |
46 | {{ extractionData.key_status.working ? "Working" : "Not working" }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-Frida/extract_hmac.py:
--------------------------------------------------------------------------------
1 | import frida, sys
2 |
3 | def on_message(message, data):
4 | if message['type'] == 'send':
5 | print("[*] {0}".format(message['payload']))
6 | else:
7 | print(message)
8 |
9 | jscode = """
10 | const DEBUG = false;
11 |
12 | function stringFromByteArray(bArray) {
13 | const extraByteMap = [ 1, 1, 1, 1, 2, 2, 3, 0 ];
14 | var count = bArray.length;
15 | var str = "";
16 |
17 | for (var index = 0;index < count;)
18 | {
19 | var ch = bArray[index++];
20 | if (ch & 0x80)
21 | {
22 | var extra = extraByteMap[(ch >> 3) & 0x07];
23 | if (!(ch & 0x40) || !extra || ((index + extra) > count))
24 | return null;
25 |
26 | ch = ch & (0x3F >> extra);
27 | for (;extra > 0;extra -= 1)
28 | {
29 | var chx = bArray[index++];
30 | if ((chx & 0xC0) != 0x80)
31 | return null;
32 |
33 | ch = (ch << 6) | (chx & 0x3F);
34 | }
35 | }
36 |
37 | str += String.fromCharCode(ch);
38 | }
39 | return str;
40 | }
41 |
42 | Java.perform(function () {
43 |
44 | var alreadyPrinted = false;
45 |
46 | Java.use('com.jodelapp.jodelandroidv3.JodelApp').onCreate.overload().implementation = function() {
47 | this.onCreate();
48 | console.log("Version: " + this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName.value);
49 | }
50 |
51 | Java.use('javax.crypto.Mac').init.overload('java.security.Key').implementation = function (v) {
52 | if (!alreadyPrinted) {
53 | console.log("HMAC-Key: " + stringFromByteArray(v.getEncoded()));
54 | alreadyPrinted = true;
55 | }
56 |
57 | return this.init(v);
58 | };
59 |
60 | if (DEBUG)
61 | Java.use('javax.crypto.Mac').doFinal.overload('[B').implementation = function(toBeHmaced) {
62 | console.log("To be HMACed: " + stringFromByteArray(toBeHmaced));
63 | return this.doFinal(toBeHmaced);
64 | }
65 | });
66 | """
67 |
68 | try:
69 | device = frida.get_usb_device()
70 | pid = device.spawn(['com.tellm.android.app'])
71 | session = device.attach(pid)
72 | script = session.create_script(jscode)
73 | script.on('message', on_message)
74 | device.resume(pid)
75 | script.load()
76 | sys.stdin.read()
77 | except Exception as e:
78 | print(e)
79 |
--------------------------------------------------------------------------------
/03_Reversing_SSL-Pinning.md:
--------------------------------------------------------------------------------
1 | ## Bypass SSL-Pinning
2 | In the following, bypassing SSL/TLS-Pinning within the Jodel app is described.
3 |
4 | ### Android
5 | This script is for use with [frida](https://frida.re/). As Jodel is heavily obfuscated, bypassing the TLS pinning needs to be done a little different. Method names in the Jodel app are unicode characters which are not directly usable in frida. A possible workaround is shown below. Just copy & paste the unicode character method name of your current Jodel version in the script and run it. This also circumvents certificate validation.
6 |
7 | Due to the obfuscation, finding the correct class and method is not always easy. Therefore you might want to try to search for the strings contained in the method, e.g. `unsupported hashAlgorithm: ` (see [here](https://github.com/square/okhttp/blob/3ad1912f783e108b3d0ad2c4a5b1b89b827e4db9/okhttp/src/jvmMain/kotlin/okhttp3/CertificatePinner.kt#L177)). Just check the source file for more strings if something changes over time.
8 |
9 | The method can be easily identified as it is the only method accepting a `String` and `Function0` (Kotlin) as parameters.
10 |
11 | ```
12 | import frida, sys, time
13 |
14 | def on_message(message, data):
15 | if message['type'] == 'send':
16 | print("[*] {0}".format(message['payload']))
17 | else:
18 | print(message)
19 |
20 | jscode = """
21 | Java.perform(function () {
22 | let b = Java.use("okhttp3.b");
23 | b["b"].overload('java.lang.String', 'kotlin.jvm.functions.Function0').implementation = function (str, function0) {
24 | console.log('OkHTTP 3.x check() called. Not throwing an exception.');
25 | };
26 | })
27 | """
28 |
29 | pid = frida.get_usb_device().spawn('com.tellm.android.app')
30 | frida.get_usb_device().resume(pid)
31 | #time.sleep(1) #Without it Java.perform silently fails
32 | session = frida.get_usb_device().attach(pid)
33 | script = session.create_script(jscode)
34 | script.on('message', on_message)
35 | print('Running...')
36 | script.load()
37 | sys.stdin.read()
38 | ```
39 |
40 | Keep in mind that Android version 7 and above only accepts certificates installed in the system CA store (and ignores the one installed by the user). To circumvent this restriction patching the NetworkSecurityConfig is one possibility, another is utilizing [this Magisk module](https://github.com/NVISO-BE/MagiskTrustUserCerts) to move user certificates to the system store.
41 |
42 | ### iOS
43 | For iOS, TLS pinning is done by using the respecitve system APIs. Multiple Cydia packages exist to circumvent such pinning mechanisms. The package [SSL Kill Switch 2](https://github.com/nabla-c0d3/ssl-kill-switch2) should do the trick here.
44 |
45 | Alternatively you can use the [objection](https://github.com/sensepost/objection) frameworks command `ios sslpinning disable` which should have the same effect.
46 |
--------------------------------------------------------------------------------
/01_Reversing_App.md:
--------------------------------------------------------------------------------
1 | ## Analysis of the application
2 |
3 | ### The APK-File
4 | Only a few details here:
5 | - The app is most probably obfuscated using [DexGuard](https://www.guardsquare.com/en/products/dexguard)
6 | - Heavy control-flow obfuscation by stub math operations
7 | - Heavy method name obfuscation, method names are not human readable anymore, mostly special characters or weired white-spaces
8 | - Ressource names are scrambled and in raw apk (not packed in asrc file)
9 | - Filenames are in non-ASCII characters
10 | - Extracting the APK under windows os will result in errors as windows makes no difference between unicode
11 | spaces like `U+0020` (Unicode space) and `U+00A0` (Unicode non-breaking space) and so on. This results in filename collisions and owerwriting of files.
12 | - Resource ids were mostly stripped out of the xml file, no chance of recovering them
13 |
14 | ### Unpacking / Repacking
15 | - Unpacking / repacking should work using latest version of [apktool](https://ibotpeaches.github.io/Apktool/) under linux-based OS
16 | - Simply re-signing the APK will result in a [HMAC signature error](#HMAC-signature-error). This is caused by a invalid signature used by the libhmac.so to decrypt the HMAC-Key (see the HMAC-reversing). A possible workaroud is implementing the HMAC-routine on your own [like in JodelPatched](https://github.com/JodelRaccoons/JodelPatched/blob/master/patched/src/main/java/com/jodelapp/jodelandroidv3/api/HmacInterceptor.java). Process would be as follows:
17 | 1. Compile or grab 'latest' JodelPatched apk
18 | 2. Decompile the JodelPatched apk to smali code
19 | 3. Extract the classes*.dex from the original Jodel APK
20 | 4. Convert it to smali using latest [baksmali](https://bitbucket.org/JesusFreke/smali/downloads/) (Steps 3 and 4 are to circumvent problems with the scrambled ressources)
21 | 5. Edit HmacInterceptor.smali by copy&pasting the modified methods from the decompiled JodelPatched apk. Double check syntax and registers used to avoid complications with the smalivm.
22 | 6. Recompile dex with [smali](https://bitbucket.org/JesusFreke/smali/downloads/), pack into apk, sign with [dex2jar-apk-sign.sh](https://github.com/pxb1988/dex2jar) and install on device
23 | 7. If you did all correct: profit!!!
24 |
25 | ##### HMAC signature error
26 | ```
27 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0x8e
28 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] string: '��M�L�n�@�}[
29 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] )Ί;0=�m��H|�c`}��'
30 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] in call to NewStringUTF
31 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] from byte[] com.jodelapp.jodelandroidv3.api.HmacInterceptor.sign(java.lang.String, java.lang.String, byte[])
32 | ```
33 |
34 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v2/jodel.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from idautils import *
3 | from idaapi import *
4 | from idc import *
5 | import sys, traceback
6 | import decrypt
7 | import re
8 | import ida_hexrays
9 |
10 | FUNCTION_PATTERN = 'Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_init'
11 |
12 | #REGEX_EXTRACT_BYTES = r'(?<=[^ ] )\d\w*'
13 | REGEX_EXTRACT_BYTES = r'#0x\d\w*'
14 | REGEX_EXTRACT_LOCATION = r'(?<=_)\w*'
15 |
16 |
17 | def locate_function():
18 | for segea in Segments():
19 | for funcea in Functions(segea, idc.get_segm_end(segea)):
20 | functionName = idc.get_func_name(funcea)
21 | if FUNCTION_PATTERN in functionName:
22 | return funcea
23 | return None
24 |
25 |
26 | def get_disassembly(funcea):
27 | disasm = []
28 | for (startea, endea) in Chunks(funcea):
29 | for head in Heads(startea, endea):
30 | disasm.append(GetDisasm(head))
31 | return disasm
32 |
33 |
34 | def extract_bytes():
35 | instr = {}
36 | target_function = locate_function()
37 | print("Found function with pattern ",FUNCTION_PATTERN, " at ", target_function)
38 | # get decompiled function
39 | decompiled_function = ida_hexrays.decompile(target_function)
40 | # get result as string and only match lines containing 0x
41 | decompiled_instructions = [instr for instr in str(decompiled_function).split("\n") if '0x' in instr]
42 | # remove prefixes in lines
43 | decompiled_instructions = [re.sub(r'(byte_|unk_|dword_)', '', instr) for instr in decompiled_instructions]
44 | # remove whitespaces
45 | decompiled_instructions = [re.sub(r'(\s)', '', instr) for instr in decompiled_instructions]
46 | # remove semicolons
47 | decompiled_instructions = [re.sub(r'(;)', '', instr) for instr in decompiled_instructions]
48 | # remove 0x prefix
49 | decompiled_instructions = [re.sub(r'(0x)', '', instr) for instr in decompiled_instructions]
50 |
51 | instructions_dict = dict((int(instr.split("=")[0], 16), rev(str(instr.split("=")[1]))) for instr in decompiled_instructions)
52 |
53 | sorted_keys = ''.join(OrderedDict(sorted(instructions_dict.items())).values())
54 |
55 | print(sorted_keys)
56 |
57 | import decrypt
58 | if len(sorted_keys) != decrypt.CLIENT_SECRET_SIZE*2:
59 | print("Keysize is {}, should be {} exiting".format(len(sorted_keys), decrypt.CLIENT_SECRET_SIZE*2))
60 | return None
61 | return [int(sorted_keys[x:x + 2], 16) for x in range(0, len(sorted_keys), 2)]
62 |
63 |
64 | def rev(a):
65 | new = ""
66 | for x in range(-1, -len(a), -2):
67 | new += a[x-1] + a[x]
68 | return new
69 |
70 |
71 | if __name__ == '__main__':
72 | try:
73 | key = extract_bytes()
74 | if key:
75 | print('Derived key of length {} from library, now decrypting it...'.format(len(key)))
76 | print('Got raw key array: {}'.format(key))
77 | _result = decrypt.decrypt(key)
78 | print('Got decrypted key: {}'.format(_result))
79 | except Exception as e:
80 | print('Exception: {}'.format(e))
81 | print(traceback.print_exc(file=sys.stdout))
82 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-redundant-jsdoc": true,
69 | "no-shadowed-variable": true,
70 | "no-string-literal": false,
71 | "no-string-throw": true,
72 | "no-switch-case-fall-through": true,
73 | "no-trailing-whitespace": true,
74 | "no-unnecessary-initializer": true,
75 | "no-unused-expression": true,
76 | "no-use-before-declare": true,
77 | "no-var-keyword": true,
78 | "object-literal-sort-keys": false,
79 | "one-line": [
80 | true,
81 | "check-open-brace",
82 | "check-catch",
83 | "check-else",
84 | "check-whitespace"
85 | ],
86 | "prefer-const": true,
87 | "quotemark": [
88 | true,
89 | "single"
90 | ],
91 | "radix": true,
92 | "semicolon": [
93 | true,
94 | "always"
95 | ],
96 | "triple-equals": [
97 | true,
98 | "allow-null-check"
99 | ],
100 | "typedef-whitespace": [
101 | true,
102 | {
103 | "call-signature": "nospace",
104 | "index-signature": "nospace",
105 | "parameter": "nospace",
106 | "property-declaration": "nospace",
107 | "variable-declaration": "nospace"
108 | }
109 | ],
110 | "unified-signatures": true,
111 | "variable-name": false,
112 | "whitespace": [
113 | true,
114 | "check-branch",
115 | "check-decl",
116 | "check-operator",
117 | "check-separator",
118 | "check-type"
119 | ],
120 | "no-output-on-prefix": true,
121 | "use-input-property-decorator": true,
122 | "use-output-property-decorator": true,
123 | "use-host-property-decorator": true,
124 | "no-input-rename": true,
125 | "no-output-rename": true,
126 | "use-life-cycle-interface": true,
127 | "use-pipe-transform-interface": true,
128 | "component-class-suffix": true,
129 | "directive-class-suffix": true
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/frontend-src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills.
22 | * This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot
23 | */
24 |
25 | // import 'core-js/es6/symbol';
26 | // import 'core-js/es6/object';
27 | // import 'core-js/es6/function';
28 | // import 'core-js/es6/parse-int';
29 | // import 'core-js/es6/parse-float';
30 | // import 'core-js/es6/number';
31 | // import 'core-js/es6/math';
32 | // import 'core-js/es6/string';
33 | // import 'core-js/es6/date';
34 | // import 'core-js/es6/array';
35 | // import 'core-js/es6/regexp';
36 | // import 'core-js/es6/map';
37 | // import 'core-js/es6/weak-map';
38 | // import 'core-js/es6/set';
39 |
40 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
41 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
42 |
43 | /** IE10 and IE11 requires the following for the Reflect API. */
44 | // import 'core-js/es6/reflect';
45 |
46 | /**
47 | * Web Animations `@angular/platform-browser/animations`
48 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
49 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
50 | */
51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
52 |
53 | /**
54 | * By default, zone.js will patch all possible macroTask and DomEvents
55 | * user can disable parts of macroTask/DomEvents patch by setting following flags
56 | * because those flags need to be set before `zone.js` being loaded, and webpack
57 | * will put import in the top of bundle, so user need to create a separate file
58 | * in this directory (for example: zone-flags.ts), and put the following flags
59 | * into that file, and then add the following code before importing zone.js.
60 | * import './zone-flags.ts';
61 | *
62 | * The flags allowed in zone-flags.ts are listed here.
63 | *
64 | * The following flags will work for all browsers.
65 | *
66 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
67 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
68 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
69 | *
70 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
71 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
72 | *
73 | * (window as any).__Zone_enable_cross_context_check = true;
74 | *
75 | */
76 |
77 | /***************************************************************************************************
78 | * Zone JS is required by default for Angular itself.
79 | */
80 | import 'zone.js/dist/zone'; // Included with Angular CLI.
81 |
82 |
83 | /***************************************************************************************************
84 | * APPLICATION IMPORTS
85 | */
86 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v2/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Jodel Keyhack v2
3 |
4 | Tested with
5 | - IDA 7.7
6 | - Python3 support is required
7 | - The Android version requires a hexrays decompiler license
8 | - Jodel 8.0.1 arm64-v8a (Android)
9 | - Jodel version 4.139 and version 7.51 (iOS)
10 |
11 | ## iOS
12 | Just a small script to use with IDA to extract the HMAC key from a decrypted binary. The key extraction the script performs is pretty straightforward:
13 | - Locating the `+[JDLCrypto convertNSStringToCString:]` function
14 | - Decompile the function AFTER the `+[JDLCrypto convertNSStringToCString:]` function (usually `sub_10004F2E8` or simmilar), here a reference to the XORed key is present
15 | - Look for the first MOV instruction (which contains the reference to the XORed key) and extract the address of the XORed key
16 | - XOR the key with the static value `ed25b40c912702e08c2b2a06eae635e03f475cc3` (extracted from the app)
17 |
18 | #### How to use
19 | - Save the `jodel_ios.py` file somewhere to your local drive
20 | - Fire up IDA and load a **DECRYPTED** Jodel binary into it
21 | - Wait for the initial IDA analysis to finish
22 | - Hit ALT+F7 or choose `File -> Script File`
23 | - Choose the `jodel_ios.py` file
24 | - Profit
25 |
26 | Running the script should provide the following output:
27 |
28 | ```
29 | [...]
30 | Found function +[JDLCrypto convertNSStringToCString:] at 4295128280
31 | Found function with pattern convertNSStringToCString at 4295128424
32 | Found function +[JDLCrypto convertNSStringToCString:] at 4295128280
33 | Got raw key: 3c21795415577f264e4b5b505f4413677d2559206436607f16062d5e5d7c235d552b40515f3a2f60
34 | Decrypted key: YEKawcOEwzigovvWEFkBVWPIsgHhnIFmfMtfjYLS
35 | ```
36 |
37 | ## Android
38 |
39 | ### What this is
40 | This is just a small IDA script, extracting the HMAC key from the library and decoding it as [cfib90s](https://bitbucket.org/cfib90/) script was broken for me.
41 |
42 | The approach of this script is scraping the bare `mov` instructions, stripping the key from them and unscrambling it.
43 | As the key is somehow XORed with a scrambled version of the APKs signature the decrypt.py (ported by tm, based on the OJOC-Keyhack) unscrambles and XORs it.
44 |
45 | This script was developed and tested with IDA 7.7 but should also work on other versions.
46 |
47 | ### Requirements
48 | - IDA Pro version 7.0 or higher
49 | - Any version of the Jodel APK
50 | - Hexrays decompiler license
51 |
52 | ### How to use
53 | - Clone this repo or download the `jodel.py` and `decrypt.py`
54 | - Fire up IDA 7.0 or later
55 | - Feed it with the latest libhmac.so (arm64)
56 | - Open the APK with 7-Zip or simmilar
57 | - Extract the file /lib/x86/libX.so where X is a random lowercase character
58 | - The correct library file is around 200 kb
59 | - Wait for the initial IDA analysis to finish
60 | - Hit ALT+F7 or choose `File -> Script File`
61 | - A file explorer should open, choose the `jodel.py` file
62 | - The IDA console should display the extracted HMAC key
63 |
64 | ### Example output
65 | ```
66 | ---------------------------------------------------------------------------------------------
67 | Python 3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)]
68 | IDAPython 64-bit v7.4.0 final (serial 0) (c) The IDAPython Team
69 | ---------------------------------------------------------------------------------------------
70 | Found function with pattern Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_init at 54624
71 | C6DEAEB09FFCCE3AB116032DB95617381927B96672E02A271F5957B2AF7CD400C67A97378BCF34B4
72 | Derived key of length 40 from library, now decrypting it...
73 | Got raw key array: [198, 222, 174, 176, 159, 252, 206, 58, 177, 22, 3, 45, 185, 86, 23, 56, 25, 39, 185, 102, 114, 224, 42, 39, 31, 89, 87, 178, 175, 124, 212, 0, 198, 122, 151, 55, 139, 207, 52, 180]
74 | Got decrypted key: b'PohIBVvuWFhSLydTFZSjDMWmHrpRQuEGEBPfgIxB'
75 | ```
76 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/backend/r2instance.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 |
3 | import r2pipe
4 | import re
5 |
6 | R2_LIST_FUNCTIONS = 'afl'
7 | R2_DISASSEMBLE_INSTRUCTIONS = 's {}; pi 25'
8 |
9 | REGEX_EXTRACT_BYTES = r'(?<=[^ ] )\d\w*'
10 | REGEX_FIND_FUNCTIONS = r'fcn.\w+'
11 |
12 |
13 | def rev(a):
14 | new = ""
15 | for x in range(-1, -len(a), -2):
16 | new += a[x - 1] + a[x]
17 |
18 | return new
19 |
20 |
21 | class R2Instance:
22 | def __init__(self, path):
23 | self.r2 = r2pipe.open(path)
24 | self.r2.cmd('aaa')
25 | self.is_correct_binary = False
26 |
27 | self.key = self.get_method_name()
28 | if self.key is not False:
29 | print("Correct binary is {}".format(path))
30 | self.is_correct_binary = True
31 |
32 | def __enter__(self):
33 | return self
34 |
35 | def __exit__(self, type, value, traceback):
36 | pass
37 |
38 | def __del__(self):
39 | self.r2.quit()
40 |
41 | def get_method_name(self):
42 | func = self.r2.cmd(R2_LIST_FUNCTIONS).split('\r\n')
43 | regexp = re.compile(REGEX_FIND_FUNCTIONS)
44 | functions = []
45 | for f in func:
46 | reg_res = regexp.search(f)
47 | if reg_res:
48 | functions.append(reg_res.group(0))
49 |
50 |
51 | possibly_correct1 = []
52 | for f in functions:
53 | len = int(self.r2.cmd('s {};pif~?'.format(f)).rstrip())
54 | if len == 21:
55 | possibly_correct1.append(f)
56 |
57 | print('PossiblyCorrect1: {}'.format(possibly_correct1))
58 |
59 | count_mov_byte = 8 #should be 8
60 | count_mov_dword = 8 # should be 8
61 | count_add_eax = 1 #should be 1
62 | count_pop_eax = 1 #should be 1
63 |
64 | possibly_correct2 = []
65 | for f in possibly_correct1:
66 | instr = self.r2.cmd('s {};pi 18'.format(f))
67 | _count_move_byte = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('mov byte'), instr))
68 | _count_move_dword = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('mov dword'), instr))
69 | _count_add_eax = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('add eax'), instr))
70 | _count_pop_eax = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('pop eax'), instr))
71 |
72 | if _count_move_byte == count_mov_byte and _count_move_dword == count_mov_dword and _count_add_eax == count_add_eax and _count_pop_eax == count_pop_eax:
73 | possibly_correct2.append(f)
74 |
75 | for f in possibly_correct2:
76 | try:
77 | return self.extract_bytes(f)
78 | except Exception as e:
79 | pass
80 |
81 | return False
82 |
83 | def extract_bytes(self, function_name):
84 | instr = {}
85 | # https://memegenerator.net/img/instances/75909642/how-does-this-even-work.jpg
86 | instructions = [d for d in self.r2.cmd(R2_DISASSEMBLE_INSTRUCTIONS.format(
87 | function_name)).split('\r') if 'mov' in d and 'eax' in d]
88 | for i in instructions:
89 | matches = re.findall(REGEX_EXTRACT_BYTES, i)
90 | value = matches[1].replace('0x', '').strip()
91 | if len(value) <= 1 or (8 > len(value) > 2):
92 | value = '0' + value
93 | if len(value) > 8 and value.startswith('0'):
94 | value = value[1:]
95 | instr[int(matches[0], 0)] = rev(value)
96 |
97 | sorted_keys = ''.join(OrderedDict(sorted(instr.items())).values())
98 |
99 | import decrypt
100 | if len(sorted_keys) != decrypt.CLIENT_SECRET_SIZE*2:
101 | print("Keysize is {}, exiting".format(len(sorted_keys)))
102 | return [int(sorted_keys[x:x + 2], 16) for x in range(0, len(sorted_keys), 2)]
103 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/backend/server.py:
--------------------------------------------------------------------------------
1 | import shutil, zipfile, jodel_api, tempfile, os, time
2 |
3 | from flask import Flask, request, redirect, url_for
4 | from werkzeug.utils import secure_filename
5 | import decrypt as decrypt
6 | from r2instance import R2Instance
7 | from pyaxmlparser import APK
8 |
9 | UPLOAD_FOLDER = tempfile.gettempdir()
10 | ALLOWED_EXTENSIONS = {'apk'}
11 |
12 | app = Flask(__name__, static_url_path="/static", static_folder="../frontend-dist")
13 |
14 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
15 |
16 |
17 | @app.route('/', methods=['GET'])
18 | def index():
19 | return redirect(url_for('static', filename='index.html'))
20 |
21 |
22 | @app.route('/api/upload', methods=['POST'])
23 | def upload():
24 | if 'file' not in request.files:
25 | return redirect(request.url)
26 | file = request.files['file']
27 | if file.filename == '':
28 | return redirect(request.url)
29 | if file and allowed_file(file.filename):
30 | filename = secure_filename(file.filename)
31 | filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
32 | file.save(filepath)
33 | return jodel_api.json.dumps(process_file(filepath))
34 | else:
35 | return {'error':True, 'message': 'File type not allowed!'}
36 |
37 |
38 | def allowed_file(filename):
39 | return '.' in filename and \
40 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
41 |
42 |
43 | def gather_apk_information(apk_file_path):
44 | try:
45 | apk = APK(apk_file_path)
46 | return {'error':False, 'package': apk.package, 'version_name': apk.version_name,
47 | 'version_code': apk.version_code,
48 | 'is_jodel_signature': True if apk.package == 'com.tellm.android.app' else False}
49 | except:
50 | return {'error':True, 'message': 'Failed verifying APK file!'}
51 |
52 |
53 | def process_file(apk_file_path):
54 | apk_information = gather_apk_information(apk_file_path)
55 | if not apk_information['error']:
56 | r2instance, unzip_directory = extract_zip(apk_file_path)
57 | clean_up_mess(apk_file_path, unzip_directory)
58 | if r2instance is None:
59 | return {'error':True, 'message': 'Library file not found, exiting...'}
60 | apk_information['hmac_key'] = decrypt.decrypt(r2instance.key).decode("utf-8")
61 | apk_information['key_status'] = is_key_working(apk_information['hmac_key'], apk_information['version_name'])
62 | apk_information['error'] = False
63 | apk_information['message'] = 'Successfully extracted key!'
64 |
65 | return apk_information
66 |
67 |
68 | def clean_up_mess(apk_file_path, extracted_file_path):
69 | try:
70 | if apk_file_path and os.path.isfile(apk_file_path):
71 | os.remove(apk_file_path)
72 | print('Removed APK file')
73 |
74 | if extracted_file_path and os.path.isdir(extracted_file_path):
75 | shutil.rmtree(extracted_file_path)
76 | print('Removed extracted files')
77 | except:
78 | print('failed to remove files')
79 |
80 |
81 | def extract_zip(path):
82 | with zipfile.ZipFile(path) as archive:
83 | unzip_directory = os.path.join(UPLOAD_FOLDER, str(time.time()))
84 | for file in archive.namelist():
85 | if file.startswith('lib/') and file.find('x86') != -1:
86 | extracted_file = os.path.join(
87 | unzip_directory, archive.extract(file, unzip_directory))
88 | _r2instance = R2Instance(extracted_file)
89 | if _r2instance.is_correct_binary:
90 | return _r2instance, unzip_directory
91 | else:
92 | del _r2instance
93 |
94 | return None, unzip_directory
95 |
96 |
97 | def is_key_working(key, version):
98 | try:
99 | lat, lng, city = 48.148900, 11.567400, "Munich"
100 | j = jodel_api.JodelAccount(lat=lat, lng=lng, city=city)
101 | return {'working': True, 'account': j.get_account_data()}
102 | except Exception as e:
103 | print(e)
104 | return {'working': False}
105 |
106 |
107 | if __name__ == '__main__':
108 | Flask.run(app, debug=False)
109 |
110 |
--------------------------------------------------------------------------------
/Jodel-Keyhack-v3/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "my-project": {
7 | "root": "",
8 | "sourceRoot": "frontend-src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {
12 | "@schematics/angular:component": {
13 | "styleext": "less"
14 | }
15 | },
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "outputPath": "frontend-dist",
21 | "index": "frontend-src/index.html",
22 | "main": "frontend-src/main.ts",
23 | "polyfills": "frontend-src/polyfills.ts",
24 | "tsConfig": "frontend-src/tsconfig.app.json",
25 | "assets": [
26 | "frontend-src/favicon.ico",
27 | "frontend-src/assets",
28 | {
29 | "glob": "**/*",
30 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
31 | "output": "/assets/"
32 | }
33 | ],
34 | "styles": [
35 | "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",
36 | "frontend-src/styles.less"
37 | ],
38 | "scripts": []
39 | },
40 | "configurations": {
41 | "production": {
42 | "fileReplacements": [
43 | {
44 | "replace": "frontend-src/environments/environment.ts",
45 | "with": "frontend-src/environments/environment.prod.ts"
46 | }
47 | ],
48 | "optimization": true,
49 | "outputHashing": "all",
50 | "sourceMap": false,
51 | "extractCss": true,
52 | "namedChunks": false,
53 | "aot": true,
54 | "extractLicenses": true,
55 | "vendorChunk": false,
56 | "buildOptimizer": true,
57 | "budgets": [
58 | {
59 | "type": "initial",
60 | "maximumWarning": "2mb",
61 | "maximumError": "5mb"
62 | }
63 | ]
64 | },
65 | "hmr": {
66 | "fileReplacements": [
67 | {
68 | "replace": "frontend-src/environments/environment.ts",
69 | "with": "frontend-src/environments/environment.hmr.ts"
70 | }
71 | ]
72 | }
73 | }
74 | },
75 | "serve": {
76 | "builder": "@angular-devkit/build-angular:dev-server",
77 | "options": {
78 | "browserTarget": "my-project:build",
79 | "proxyConfig": "frontend-src/proxy.conf.json"
80 | },
81 | "configurations": {
82 | "production": {
83 | "browserTarget": "my-project:build:production"
84 | },
85 | "hmr": {
86 | "hmr": true,
87 | "browserTarget": "my-project:build:hmr"
88 | }
89 | }
90 | },
91 | "extract-i18n": {
92 | "builder": "@angular-devkit/build-angular:extract-i18n",
93 | "options": {
94 | "browserTarget": "my-project:build"
95 | }
96 | },
97 | "test": {
98 | "builder": "@angular-devkit/build-angular:karma",
99 | "options": {
100 | "main": "frontend-src/test.ts",
101 | "polyfills": "frontend-src/polyfills.ts",
102 | "tsConfig": "frontend-src/tsconfig.spec.json",
103 | "karmaConfig": "frontend-src/karma.conf.js",
104 | "styles": [
105 | "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",
106 | "frontend-src/styles.less"
107 | ],
108 | "scripts": [],
109 | "assets": [
110 | "frontend-src/favicon.ico",
111 | "frontend-src/assets"
112 | ]
113 | }
114 | },
115 | "lint": {
116 | "builder": "@angular-devkit/build-angular:tslint",
117 | "options": {
118 | "tsConfig": [
119 | "frontend-src/tsconfig.app.json",
120 | "frontend-src/tsconfig.spec.json"
121 | ],
122 | "exclude": [
123 | "**/node_modules/**"
124 | ]
125 | }
126 | }
127 | }
128 | }
129 | },
130 | "defaultProject": "my-project"
131 | }
--------------------------------------------------------------------------------
/02_Reversing_HMAC.md:
--------------------------------------------------------------------------------
1 | ## How does the HMAC-Signing in Jodel work?
2 |
3 | First of all read [this](https://en.wikipedia.org/wiki/HMAC)! It's important to understand what HMAC is used for in order to understand what Jodel is doing there.
4 |
5 | Each version of the Jodel app has a version-specific HMAC key. It is used to sign requests, the signature is checked by the Jodel API server. HMAC (Keyed-Hash Message Authentication Code) is a mechanism that generates a signature of given data (in our case HTTP request data) in combination with a key. Without knowledge of the HMAC key, it is impossible to sign requests and therefore it is impossible to use the Jodel API.
6 |
7 | ### Locating the HMAC signing library
8 |
9 | Since the HMAC key is an important measure to prevent attacks on the Jodel API, the HMAC key is well hidden in the application. Jodel follows best practices (see MASVS) and hides the key in a shared library bundled with the APK file. The name of the shared library responsible for generating the HMAC key is renamed in each build (/lib/\/libX.so), determining which is the correct one is only possible based on file sizes:
10 |
11 | | architecture | approx. size |
12 | |--- |--- |
13 | | x86 | 194 KB |
14 | | x86-64 | 219 KB |
15 | | armeabi-v7a | 102 KB |
16 | | arm64-v8a | 199 KB |
17 |
18 | (as of 09.04.2020, Jodel version 5.77.0, libb.so)
19 |
20 | ### What's inside
21 | The signing inside the Jodel application works as follows:
22 |
23 | The class `com.jodelapp.jodelandroidv3.api.HmacInterceptor` is used for signing the requests. It has three methods which calls the shared library using the Java native interface:
24 | ```
25 | - private native void init();
26 | - private native synchronized void register(String str);
27 | - private native synchronized byte[] sign(String str, String str2, byte[] bArr);
28 | ```
29 |
30 | ### The shared library
31 | Method names in the shared library are (as of v. 5.77.0) generated during runtime. Therefore a static analysis using tools like IDA does not reveal the method names. Looking at older versions of the Jodel app shows the following methods:
32 |
33 | ##### private native void init();
34 | This method calls the native method `sym.Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_init` in the corresponding shared object. The native method generates the HMAC-Key at runtime.
35 |
36 | Reading the assembler code (of the x86 binary) looks similar to this:
37 | ```
38 |
39 | mov byte [eax + 0x198], 0x95
40 | mov dword [eax + 0x194], 0x9f8effc2
41 | mov byte [eax + 0x19d], 4
42 | mov dword [eax + 0x199], 0x8c0dd9e9
43 |
44 | ```
45 |
46 | Thinking of `eax` as the address of a bytearray, the assembler code just initializes a byte array with given values. The values can be considered the _"encrypted"_ (XORed with the applications signature) key.
47 |
48 | ##### private native synchronized void register(String str);
49 | This method calls the native method `sym.Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_register`. It takes one string parameter which describes what kind of request is going to be signed in one of the next `sign()` calls. These strings look like the following example:
50 |
51 | ```
52 | GET@/api/v3/user/config
53 | ```
54 |
55 | This could be the implementation of a queue. A signing request is passed and queued in a std::map. The signing routine uses the values of the map to locate the signing request.
56 |
57 | ##### private native synchronized byte[] sign(String sig, String method, byte[] payload);
58 | The `sign()` method of the HmacInterceptor class performs the signing itself. It calls the native method `sym.Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_sign`. It takes three arguments:
59 | ```
60 | sig: The APKs SHA1 signature: a4a8d4d7b09736a0f65596a868cc6fd620920fb0 (should be always this value!)
61 | method: Same string which gets called to register(String str): GET@/api/v3/user/recommendedChannels
62 | payload: GET%api.go-tellm.com%443%/api/v3/posts/location/combo%39422506-d25adbe9-4c85ef1a-1dca-4771-abd7-249e4eb16047%49.6679;9.9074%2019-01-12T12:03:32Z%channels%true%home%false%lat%49.667938232421875%lng%9.907393455505371%radius%true%skipHometown%false%stickies%true%
63 | ```
64 |
65 |
66 | The pseudo code of the `sign()` function looks like this:
67 |
68 | ```
69 | int Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_sign(JNIEnv *env, jobject jobj, char *sig_1, char *key_in_map, char *hmac_input) {
70 | secretKey = signature ^ xor_key; //a little more complicated, see the decryption routine
71 |
72 | // this Java calls are done from the shared library, this is only the Java part of it
73 | SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
74 | Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
75 | mac.init(signingKey);
76 | return toHexString(mac.doFinal(data.getBytes()));
77 | }
78 | ```
79 |
80 | ---
81 |
82 | ### Extracting the HMAC-Key
83 |
84 | There are multiple methods extracting the HMAC key:
85 |
86 | Good old static way
87 | - Extracting the hmac-key the static way requires the hmac-signing library (as the base key is in there) and some decryption magic. There are several projects doing this, some of them work, some dont. The ones i'm aware of are:
88 | - [ojoc-keyhack by cfib90](https://bitbucket.org/cfib90/ojoc-keyhack) (the original one, utilizing objdump)
89 | - [Jodel-Keyhack-v2](https://github.com/JodelRaccoons/JodelReversing/blob/master/Jodel-Keyhack-v2) utilizing IDA Pro 7.x
90 | - [Jodel-Keyhack-v3](https://github.com/JodelRaccoons/JodelReversing/blob/master/Jodel-Keyhack-v3) this project, utilizing radare2
91 |
92 | #### Dynamic (runtime hooking ftw!)
93 |
94 | - As the native library is NOT implementing their own HMAC-Signing function, they are using the one javax.crypto classes. Hooking them using librarys like frida is [pretty easy](https://github.com/JodelRaccoons/JodelReversing/tree/master/Jodel-Keyhack-Frida) (See Readme for instructions)
95 |
96 | ---
97 |
98 |
99 | As of that, i wrote a python script which disassembles the shared object, collects the bytes and decrypts it (credits for the decryption logic to [cfib90](https://bitbucket.org/cfib90/ojoc-keyhack)), the python implementation of the decryption routine was coded by tm. To make it look better i developed this keyhack with fancy angular gui.
100 |
101 | ---
102 |
103 |
104 | ## How to get the key
105 | - lokalisieren der funktionen
106 | - Funktionsnamen sind obfuskiert
107 | - string suche nach (Ljava/lang/String;)Ljavax/crypto/Mac;
108 | - xrefs auf (Ljava/lang/String;)Ljavax/crypto/Mac; finden, funktion ist sign()
109 | - funktionslayout ist init, register, sign -> zwei funktionen weiter oben ist init() mit dem hmac key
110 | - Statische analyse und mir skript entschlüsseln
111 | - Link zu skript
112 | - Native library weist
113 | - Java funktion für HMAC wird aufgerufen
114 |
115 |
--------------------------------------------------------------------------------
/04_Reversing_Android_Email_Verification.md:
--------------------------------------------------------------------------------
1 | # Android account validation (by mail)
2 |
3 | - Validation is performed using E-Mail address
4 | - Process used is [Google Firebase Authentication using E-Mail links](https://firebase.google.com/docs/auth/android/email-link-auth)
5 |
6 | ### Step 1: Requesting login
7 |
8 | - Request login using Firebase
9 |
10 | ```HTTP
11 | POST /identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=AIzaSyDFUC30aJbUREs-vKefE6QmvoVL0qqOv60 HTTP/2
12 | Host: www.googleapis.com
13 | Content-Type: application/json
14 | X-Android-Package: com.tellm.android.app
15 | X-Android-Cert: A4A8D4D7B09736A0F65596A868CC6FD620920FB0
16 | Accept-Language: en-US
17 | X-Client-Version: Android/Fallback/X21000001/FirebaseCore-Android
18 | Content-Length: 236
19 | Connection: Keep-Alive
20 | Accept-Encoding: gzip, deflate
21 |
22 | {
23 | "requestType": 6,
24 | "email": "",
25 | "androidInstallApp": true,
26 | "canHandleCodeInApp": true,
27 | "continueUrl": "https:\/\/jodel.com\/app\/magic-link-fallback",
28 | "androidPackageName": "com.tellm.android.app",
29 | "androidMinimumVersion": "5.116.0"
30 | }
31 | ```
32 |
33 | - Response simply contains confirmation of success
34 |
35 | ```HTTP
36 | HTTP/2 200 OK
37 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate
38 | Pragma: no-cache
39 | Expires: Mon, 01 Jan 1990 00:00:00 GMT
40 | Content-Type: application/json; charset=UTF-8
41 | Vary: Origin
42 | Vary: X-Origin
43 | Vary: Referer
44 | Server: ESF
45 | Content-Length: 94
46 | X-Xss-Protection: 0
47 | X-Frame-Options: SAMEORIGIN
48 | X-Content-Type-Options: nosniff
49 |
50 | {
51 | "kind": "identitytoolkit#GetOobConfirmationCodeResponse",
52 | "email": ""
53 | }
54 | ```
55 |
56 | ### Step 1½: Extracting oobCode from link
57 |
58 | - The mail sent to the target address contains the "magic" link
59 | - The link itself already contains the required oobCode (out-of-band code)
60 |
61 | ```
62 | https://ae3ts.app.goo.gl/?link=https://tellm-android.firebaseapp.com/__/auth/action?apiKey%3DAIzaSyBC5AfciIsT15NSwrfhLhsLG5UtFisbeSA%26mode%3DsignIn%26oobCode%3DXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX%26continueUrl%3Dhttps://jodel.com/app/magic-link-fallback%26lang%3Den&apn=com.tellm.android.app&amv=5.116.0
63 | => URL Decode
64 | https://ae3ts.app.goo.gl/?link=https://tellm-android.firebaseapp.com/__/auth/action?apiKey=AIzaSyBC5AfciIsT15NSwrfhLhsLG5UtFisbeSA&mode=signIn&oobCode=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&continueUrl=https://jodel.com/app/magic-link-fallback&lang=en&apn=com.tellm.android.app&amv=5.116.0
65 | => extract URL parameter
66 | oobCode=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
67 | ```
68 |
69 | ### Step 2: Redeeming oobCode
70 |
71 | - The code needs to be redeemed with the corresponding mail address
72 | - Creates a new Jodel Firebase user
73 |
74 | ```HTTP
75 | POST /identitytoolkit/v3/relyingparty/emailLinkSignin?key=AIzaSyDFUC30aJbUREs-vKefE6QmvoVL0qqOv60 HTTP/2
76 | Host: www.googleapis.com
77 | Content-Type: application/json
78 | X-Android-Package: com.tellm.android.app
79 | X-Android-Cert: A4A8D4D7B09736A0F65596A868CC6FD620920FB0
80 | Accept-Language: en-US
81 | X-Client-Version: Android/Fallback/X21000001/FirebaseCore-Android
82 | Content-Length: 95
83 | Connection: Keep-Alive
84 | Accept-Encoding: gzip, deflate
85 |
86 | {
87 | "email": "",
88 | "oobCode": ""
89 | }
90 | ```
91 |
92 | - Response contains `idToken` as well as `refreshToken`
93 | - Maybe the `idToken` can already be used to register a Jodel user and save us the token refresh?
94 |
95 | ```HTTP
96 | HTTP/2 200 OK
97 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate
98 | Expires: Mon, 01 Jan 1990 00:00:00 GMT
99 | Pragma: no-cache
100 | Content-Type: application/json; charset=UTF-8
101 | Vary: Origin
102 | Vary: X-Origin
103 | Vary: Referer
104 | Server: ESF
105 | Content-Length: 1351
106 | X-Xss-Protection: 0
107 | X-Frame-Options: SAMEORIGIN
108 | X-Content-Type-Options: nosniff
109 |
110 | {
111 | "kind": "identitytoolkit#EmailLinkSigninResponse",
112 | "idToken": "",
113 | "email": "",
114 | "refreshToken": "",
115 | "expiresIn": "3600",
116 | "localId": "",
117 | "isNewUser": false
118 | }
119 | ```
120 |
121 | ### Step 3: Refreshing tokens
122 |
123 | - Not sure whether this step is neccessary
124 | - Token refresh is performed using the `refreshToken` provided in the previous response
125 |
126 | ```HTTP
127 | POST /v1/token?key=AIzaSyDFUC30aJbUREs-vKefE6QmvoVL0qqOv60 HTTP/2
128 | Host: securetoken.googleapis.com
129 | Content-Type: application/json
130 | X-Android-Package: com.tellm.android.app
131 | X-Android-Cert: A4A8D4D7B09736A0F65596A868CC6FD620920FB0
132 | Accept-Language: en-US
133 | X-Client-Version: Android/Fallback/X21000001/FirebaseCore-Android
134 | Content-Length: 273
135 | Connection: Keep-Alive
136 | Accept-Encoding: gzip, deflate
137 |
138 | {
139 | "grantType": "refresh_token",
140 | "refreshToken": ""
141 | }
142 | ```
143 |
144 | - Response contains `access_token` and `id_token` which happened to be identical
145 | - `refresh_token` can be used to get fresh access tokens for the Firebase user
146 |
147 | ```HTTP
148 | HTTP/2 200 OK
149 | Pragma: no-cache
150 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate
151 | Expires: Mon, 01 Jan 1990 00:00:00 GMT
152 | Content-Type: application/json; charset=UTF-8
153 | Vary: Origin
154 | Vary: X-Origin
155 | Vary: Referer
156 | Server: ESF
157 | Content-Length: 2237
158 | X-Xss-Protection: 0
159 | X-Frame-Options: SAMEORIGIN
160 | X-Content-Type-Options: nosniff
161 |
162 | {
163 | "access_token": "",
164 | "expires_in": "3600",
165 | "token_type": "Bearer",
166 | "refresh_token": "",
167 | "id_token": "",
168 | "user_id": "",
169 | "project_id": "425112442765"
170 | }
171 | ```
172 |
173 | ### Step 4: Creating account using firebaseJWT
174 |
175 | - Account is created using a `device_uid` as well as the `firebaseJWT` (`access_token` or `id_token` from previous response)
176 | - Request requires HMAC signing
177 |
178 |
179 | ```HTTP
180 | POST /api/v2/users/ HTTP/1.1
181 | Host: api.jodelapis.com
182 | X-Client-Type: android_x.x.x
183 | X-Api-Version: 0.2
184 | X-Timestamp: 2022-XX-XXTXX:XX:XXZ
185 | X-Authorization: HMAC 8DECAECD87546C899E6F8865AF3D61DA7F99D6C6
186 | Content-Type: application/json; charset=UTF-8
187 | Content-Length: 1711
188 | Accept-Encoding: gzip, deflate
189 | Connection: close
190 |
191 | {
192 | "firebase_uid": "jtNECbcwmfPGgQVuyKVPpsW8UIE3", // not sure whether neccesary
193 | "device_uid": "", // can probably be random
194 | "firebaseJWT": "", // from previous request
195 | "client_id": "81e8a76e-1e02-4d17-9ba0-8a7020261b26",
196 | "iid": "", // installation id, should be ok if omitted
197 | "iid_provider": "google",
198 | "location": {
199 | "city": "Town",
200 | "country": "DE",
201 | "loc_coordinates": {
202 | "lat": ,
203 | "lng":
204 | },
205 | "loc_accuracy":
206 | },
207 | "language": "en-US",
208 | "registration_data": {
209 | "provider": "branch.io",
210 | "channel": "",
211 | "campaign": "",
212 | "feature": "",
213 | "referrer_id": "",
214 | "referrer_branch_id": ""
215 | },
216 | "registration_type": "signup",
217 | "adId": "",
218 | "dId": ""
219 | }
220 | ```
221 |
222 | - Response indicates successful user registration
223 | - User is **NOT** blocked
224 | - `access_` and `refresh_token` can be used for further API requests as usual
225 |
226 | ```HTTP
227 | HTTP/1.1 200 OK
228 | Server: nginx/1.13.12 (this version is from 2018 with HIGH CVEs (CVE-2021-23017)??? might consider patching...)
229 | Content-Type: application/json; charset=utf-8
230 | Connection: close
231 | Vary: Accept-Encoding
232 | Vary: X-HTTP-Method-Override
233 | Access-Control-Allow-Origin: *
234 | X-User-Blocked: false
235 | X-Feed-Internationalizable: false
236 | X-Feed-Internationalized: false
237 | Content-Length: 2080
238 |
239 | {
240 | "access_token": "...",
241 | "refresh_token": "...",
242 | "token_type": "bearer",
243 | "expires_in": 604800,
244 | [...]
245 | ```
246 |
--------------------------------------------------------------------------------