Digging In The Vineyard, Part 1
Vine may be blowing up your Twitter feed as it is mine. Stuttery videos of coffee preparation. Brunches that have now escaped the still-image-shackles of Instagram. Naturally, cats abound. I wanted to see if I could perhaps contribute with some pre-made animations and audio of my own, but the app doesn't allow this, ostensibly by design.
This is fixable, as you can see. I didn't quite have the pleasure of using Vine on the set of The Shining. If Kubrick saw me filming on his set with an iPhone, this series of posts would probably be about the extraction of expensive electronics from the rectum.
This is the first post in a 3 part series, split up for easier consumption. I
don't program C regularly, nor is security my day job, aside from the regular
task of trying to not write code that's swiss cheese. If your experience with
gdb
and the innards of Objective-C are minimal, never fear—this stuff is
pretty reasonable to pick up if you have a decent level of knowledge about
how computers work.
Time to get started. An easy first step is to intercept the HTTP/HTTPS network
traffic, and peep on what back and forth chatter the iOS app is having with the
Vine API. My tool of choice for this purpose is mitmproxy
,
an excellent SSL capable proxy with a nice console UI. It works by generating a
CA certificate that needs to be installed on the device, after which clients on
the device generally willingly accept the certificates that mitmproxy
generates on the fly.
Installing mitmproxy
and getting the CA certificate it generated onto the
device is trivial. I used mongoose
to
serve it up and install it through Safari, but you can also email it to yourself
and open the attachment on the device.
# mitmproxy is written in Python, and distributed as a Python package
$ pip install mitmproxy
$ mitmdump
^C%
$ ls -la ~/.mitmproxy
total 32
drwxr-xr-x 6 gabriel staff 204 Feb 23 23:29 .
drwxr-xr-x+ 126 gabriel staff 4284 Feb 23 23:29 ..
-rw-r--r-- 1 gabriel staff 969 Feb 23 23:29 mitmproxy-ca-cert.cer
-rw-r--r-- 1 gabriel staff 884 Feb 23 23:29 mitmproxy-ca-cert.p12
-rw-r--r-- 1 gabriel staff 969 Feb 23 23:29 mitmproxy-ca-cert.pem
-rw-r--r-- 1 gabriel staff 1856 Feb 23 23:29 mitmproxy-ca.pem
Once a ca-cert
file is on the device and the
proxy settings have been altered
to point to the machine running mitmproxy
or mitmdump
, we're ready to
sniff the traffic. Here's a choice request, one sent when logging in from the
Vine app:
# Some headers have been omitted for brevity
$ mitmdump -vv
10.0.1.3 POST https://api.vineapp.com/users/authenticate
User-Agent: com.vine.iphone/1.0.4 (unknown, iPhone OS 5.1.1, iPhone, Scale/2.000000)
Content-Type: application/x-www-form-urlencoded; charset=utf-8
username=redacted%40example.com&password=apassword
<< 200 OK 168B
Content-Type: application/json
Content-Length: 168
{"code": "", "data": {"username": "John", "userId": 651492886386240624, "key": "651492886386240624-021dbcd0-7e46-11e2-9e96-0800200c9a66"}, "success": true, "error": ""}
Authentication is pretty straightforward, at the very least. A username and
password are posted using plain old form encoding to the /users/authenticate
path, and a chunk of JSON with a user identifier and a session key is returned.
At least one, if not both of those are likely required to be able to make a
post to the service.
Posting a Vine reveals a little more about the workings of the API:
# Some headers have been omitted for brevity, and response bodies have been truncated
10.0.1.3 PUT https://vines.s3.amazonaws.com/videos%2F686F4AE0-7E4A-11E2-9E96-0800200C9A66-52868-000016EBB255AD22_1.0.4.mp4
User-Agent: aws-sdk-iOS/1.4.4 iPhone-OS/5.1.1 en_US
Content-Type: video/mp4
Authorization: AWS AKIAJLTHEREWASAREALACCESSKEYHERE:uRrWccShnkHUlFMsIPxskxmRLnE=
[('0000000000', '00 00 00 1c 66 74 79 70 6d 70 34 32 00 00 00 01', '....ftypmp42....'), ('0000000010', '6d 70 34 31 6d 70 34 32 69 73 6f 6d 00 00 00 08', 'mp41mp42isom
<< 200 OK 0B
10.0.1.3 PUT https://vines.s3.amazonaws.com/thumbs%2F686F4AE0-7E4A-11E2-9E96-0800200C9A66-52868-000016EBB255AD22_1.0.4.mp4.jpg
Content-Type: image/jpeg
Authorization: AWS AKIAJLTHEREWASAREALACCESSKEYHERE:qYzfyi2RT9WZExMyukjlCnZA4Yc=
[('0000000000', 'ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01', '......JFIF......'), ('0000000010', '00 01 00 00 ff e1 00 58 45 78 69 66 00 00 4d 4d', '.......XExif
<< 200 OK 0B
10.0.1.3 POST https://api.vineapp.com/posts
vine-session-id: 651492886386240624-021dbcd0-7e46-11e2-9e96-0800200c9a66
videoUrl=https%3A%2F%2Fvines.s3.amazonaws.com%2Fvideos%2F686F4AE0-7E4A-11E2-9E96-0800200C9A66-52868-000016EBB255AD22_1.0.4.mp4%3FversionId%3DuoGzZYYzKRDRv8kpBvDjbx8YyOFpTH8S&thumbnailUrl=https%3A%2F%2Fvines.s3.amazonaws.com%2Fthumbs%2F686F4AE0-7E4A-11E2-9E96-0800200C9A66-52868-000016EBB255AD22_1.0.4.mp4.jpg%3FversionId%3DRwPvlVr2gVu258jRV5kxo8es.JCH7a2y&description=Test
<< 200 OK 123B
Content-Type: application/json
{"code": "", "data": {"postId": 543402198775796103, "created": "2013-02-24T06:12:14.713491"}, "success": true, "error": ""}
10.0.1.3 GET https://api.vineapp.com/timelines/graph
vine-session-id: 651492886386240624-021dbcd0-7e46-11e2-9e96-0800200c9a66
<< 200 OK 2.27kB
Content-Type: application/json
{"code": "", "data": {"count": 93, "records": [{"liked": false, "foursquareVenueId": null, "userId": 651492886386240624, "likes": {"count": 0, "records": [], "nextPage": null, "previousPage": null, "size": 10}, "postToFacebook": 0, "thumbnailUrl": "http://vines.s3.amazonaws.com/thumbs%2F686F4AE0-7E4A-11E2-9E96-0800200C9A66-52868-000016EBB255AD22_1.0.4.mp4.jpg?versionId=RwPvlVr2gVu258jRV5kxo8es.JCH7a2y", "explicitContent": 0, "verified": 0, "avatarUrl": "http://s3.amazonaws.com/vines/avatars/default.png", "comments": {"count": 0, "records": [], "nextPage": null, "previousPage": null, "si
There's a whole bunch of stuff going on in the request/response flow here. The steps look to be:
- An MPEG-4 video is uploaded from the Vine app straight to Amazon S3.
- A JPEG thumbnail of the video is uploaded straight to S3 right afterwards.
- A request is made to the Vine API. The session key obtained during the
authentication process is sent along in the
vine-session-id
header. The POST body is form encoded, and contains the URLs of the video and thumbnail, as well as the caption entered. - The app sends the user back to their stream after the video is uploaded, and we can see that our post is showing up in the timeline JSON returned.
The requests made to the Vine API itself are simple enough—cURL or your favourite language's HTTP library will make quick work of them.
However, the requests made to S3 are going to be way more of a pain in the ass. Per the S3 documentation, uploads are signed with an AWS secret key. An upload straight to the Vine S3 bucket from outside of the app probably isn't possible, since we aren't in obvious possession of the necessary credentials. (Spoiler: we eventually get the necessary credentials.)
It's at least worth a shot to see if the Vine API will accept any old values for
videoUrl
and thumbnailUrl
. Hosting my own files for this effort certainly
isn't ideal, but it's better than not being able to post at all. Here's a
chunk of Ruby using Net::HTTP
to do just that.
#!/usr/bin/env ruby
# Usage: ./vinepost.rb USERNAME PASSWORD [THUMBNAIL_URL] [VIDEO_URL]
require "net/https"
require "uri"
require 'json'
# Yield response body on success, raise otherwise
def yield_or_raise(response)
if response.is_a?(Net::HTTPSuccess)
yield(response.body)
else
raise "Bad response: #{response.inspect}, #{response.body}"
end
end
username = ARGV[0] || raise("A username is required")
password = ARGV[1] || raise("A password is required")
base_uri = URI.parse("https://api.vineapp.com/")
auth_path = "/users/authenticate"
post_path = "/posts"
default_headers = {
"Content-Type" => "application/x-www-form-urlencoded; charset=utf-8",
# Fly at least a little under the radar, eh?
"User-Agent" => "com.vine.iphone/1.0.4 (unknown, iPhone OS 5.1.1, iPhone, Scale/2.000000)"}
http = Net::HTTP.new(base_uri.host, base_uri.port)
http.use_ssl = true
# Authenticate
body = URI.encode_www_form(username: username, password: password)
response = http.post(auth_path, body, default_headers)
key = yield_or_raise(response) { |body| JSON.parse(body)["data"]["key"] }
authed_headers = default_headers.merge('vine-session-id' => key)
video_url = ARGV[3] || "http://www.example.com/test.mp4"
thumbnail_url = ARGV[2] || "http://www.example.com/test.jpg"
# Post Vine
body = URI.encode_www_form(
description: "Testing",
videoUrl: video_url,
thumbnailUrl: thumbnail_url)
response = http.post(post_path, body, authed_headers)
yield_or_raise(response) { |body| puts body }
Running this with a valid username
and password
set will ruin your day with
the following exception:
$ ./vinepost.rb testvineuser@example.com passitypassword
vinepost.rb:9:in `yield_or_raise': Bad response: #<Net::HTTPBadRequest 400 BAD REQUEST readbody=true>, {"code": 302, "data": "", "success": false, "error": "The URL provided for the video is invalid."} (RuntimeError)
from upload.rb:45:in `<main>'
Clearly, Vine is validating the provided videoUrl
—though mildly
interestingly, not the thumbnailUrl
. Just to confirm that this is indeed the
case, let's try running it again, but this time with the video_url
and
thumbnail_url
variables set to those taken from an existing post to Vine,
randomly picked off Vinepeek:
$ ./vinepost.rb testvineuser@example.com passitypassword
{"code": "", "data": {"postId": 917701731163447296, "created": "2013-02-25T05:49:16.782546"}, "success": true, "error": ""}
This, is an image of progress being made. The pilfered borrowed video
appeared in my own feed in the application. If all I wanted to do was be able to
log in and repost other Vines, I could stop here, but where's the challenge in
that?
To get those 1998 vintage Beavis and Butthead RealVideo clips onto Vine, further
deconstruction of the app needs to happen. There's a chance that the validation
on videoUrl
could be fooled—for example, perhaps there's a simple
regular expression in place that checks the URL for vines.s3.amazonaws.com
,
and another domain like vines.s3.amazonaws.com.anotherdomain.com
could slip
by.
Assuming this isn't the case though, that makes extracting the AWS keys from the app a reasonable sounding proposition to move forward with. To be able to upload to S3, the keys have to be available to Vine on the iOS device in some shape or form. The next part of this series will go into jimmying open the Vine app itself, revealing the rich unknown treasure horde of magical strings within.
Update: Check out part 2.