From e2904e8cb450286609a4b33c0147de57b527db83 Mon Sep 17 00:00:00 2001 From: fluepke Date: Fri, 4 Dec 2020 05:01:20 +0100 Subject: [PATCH] Add README.md --- README.md | 71 +++++++++++++++++++++++++++++++++++++++ collector/api.go | 77 +++++++++++++++++++++++++++++++++++++++++++ collector/api_test.go | 20 +++++++++++ 3 files changed, 168 insertions(+) create mode 100644 README.md create mode 100644 collector/api.go create mode 100644 collector/api_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e210a8 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Vodafone Station Exporter +Prometheus Exporter for the Vodafone Station CGA4233DE. + +## Reverse Engineering the login mechanism +> I am not a Javascript engineer, but it works :man_shrugging: + +Logging into the PHP application running on the CGA4233DE is made as complicated as possible. + +From the console we see: +```bash +curl 'http://192.168.100.1/api/v1/session/login' \ + -H 'Connection: keep-alive' \ + -H 'Accept: */*' \ + -H 'X-CSRF-TOKEN: ' \ + -H 'X-Requested-With: XMLHttpRequest' \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ + --data-raw 'username=admin&password=seeksalthash' \ + --compressed \ + --insecure +``` + +> CSRF seems broken, lol. Whatever - we don't care. + +reply is +```json +{"error":"ok","salt":"","saltwebui":""} +``` + +For the actual login a derived token derived from the actual password is used: +``` +curl 'http://192.168.100.1/api/v1/session/login' \ + -H 'Connection: keep-alive' \ + -H 'Accept: */*' \ + -H 'X-CSRF-TOKEN: ' \ + -H 'X-Requested-With: XMLHttpRequest' \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ + -H 'Cookie: ' \ + --data-raw 'username=admin&password=' \ + --compressed \ + --insecure +``` + +Looking at the obfuscated JavaShit (`login.js`), we see something like follows: +```js +doPbkdf2NotCoded(doPbkdf2NotCoded("", ""), "") +``` + +quick check reveals: Yes, that returns the token used for the login. :heavy_check_mark: + +Ok, so what does `doPbkdf2NotCoded` do? + +```js +function doPbkdf2NotCoded(_0x365ad6, _0x470596) { + var _0x51b261 = sjcl[_0x5bfa('0x10')][_0x5bfa('0x11')](_0x365ad6, _0x470596, 0x3e8, 0x80); + var _0x279f24 = sjcl[_0x5bfa('0xc')][_0x5bfa('0x12')]['fromBits'](_0x51b261); + return _0x279f24; +} +``` +easy, isn't it? %) +Turns out, `sjcl` is not yet another obfuscated JS function, but this [thingie](https://github.com/bitwiseshiftleft/sjcl). + +Translated to something slightly more human readable (using the JS console) +```js +function whatTheFuck(param1, param2) { + // a, b, c, d + var temp = sjcl["misc"]["pbkdf2"](param1, param2, 0x3e8, 0x80) + return sjcl["codec"]["hex"]["fromBits"](temp) +} +``` + +From here, I started the GoLang implementation. diff --git a/collector/api.go b/collector/api.go new file mode 100644 index 0000000..a199682 --- /dev/null +++ b/collector/api.go @@ -0,0 +1,77 @@ +package collector + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "golang.org/x/crypto/pbkdf2" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "strings" + "time" +) + +type VodafoneStation struct { + URL string + Password string + client *http.Client +} + +type LoginResponse struct { + Error string `json:"error"` + Salt string `json:"salt"` + SaltWebUI string `json:"saltwebui"` +} + +func NewVodafoneStation(url, password string) *VodafoneStation { + cookieJar, err := cookiejar.New(nil) + if err != nil { + panic(err) + } + return &VodafoneStation{ + URL: url, + Password: password, + client: &http.Client{ + Jar: cookieJar, + Timeout: time.Second * 2, + }, + } +} + +func (v *VodafoneStation) getLoginSalts() (*LoginResponse, error) { + requestBody := strings.NewReader("username=admin&password=seeksalthash") + response, err := v.client.Post(v.URL+"/api/v1/session/login", "application/x-www-form-urlencoded", requestBody) + if err != nil { + return nil, err + } + if response.Body != nil { + defer response.Body.Close() + } + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + loginResponse := &LoginResponse{} + err = json.Unmarshal(responseBody, loginResponse) + if err != nil { + return nil, err + } + if loginResponse.Error != "ok" { + return nil, fmt.Errorf("Got non error=ok message from vodafone station") + } + return loginResponse, nil +} + +// GetLoginPassword derives the password using the given salts +func GetLoginPassword(password, salt, saltWebUI string) string { + return DoPbkdf2NotCoded(DoPbkdf2NotCoded(password, salt), saltWebUI) +} + +// Equivalent to the JS doPbkdf2NotCoded (see README.md) +func DoPbkdf2NotCoded(key, salt string) string { + temp := pbkdf2.Key([]byte(key), []byte(salt), 0x3e8, 0x80, sha256.New) + fmt.Println(hex.EncodeToString(temp)) + return hex.EncodeToString(temp[:16]) +} diff --git a/collector/api_test.go b/collector/api_test.go new file mode 100644 index 0000000..62dfc56 --- /dev/null +++ b/collector/api_test.go @@ -0,0 +1,20 @@ +package collector_test + +import ( + "github.com/fluepke/vodafone-station-exporter/collector" + "testing" +) + +func TestDoPbkdf2NotCoded(t *testing.T) { + result := collector.DoPbkdf2NotCoded("EqAM2KtT", "2awfm2st3cej") + if result != "c2523cb6738663f9d9223c905c59cbb6" { + t.Errorf("Got %s", result) + } +} + +func TestGetLoginPassword(t *testing.T) { + loginPassword := collector.GetLoginPassword("EqAM2KtT", "2awfm2st3cej", "4hbeVQ1Z6HK2") + if loginPassword != "b000b59875d1dc81bcd9d8f658fc7e77" { + t.Errorf("Derivation of login password failed!") + } +}