Digging In The Vineyard, Part 1

Vine

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:

  1. An MPEG-4 video is uploaded from the Vine app straight to Amazon S3.
  2. A JPEG thumbnail of the video is uploaded straight to S3 right afterwards.
  3. 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.
  4. 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": ""}

Vine Feed

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.