DevelBranch
Software Research and Development

Lightweight SSH Honeypot (part 2)

Posted in Tutorials with lsh, honeypot

Giới thiệu

Trong bài viết trước Lightweight SSH Honeypot, tôi đã mô tả về một dự án cho phép tôi xác định các máy tính đang bruteforce dịch vụ SSH trên thế giới. Tuy nhiên, có một số bạn đã inbox cho fanpage của tôi và mong muốn tôi viết rõ hơn, chi tiết hơn về các thứ mà tôi đã làm. Bài viết này tôi sẽ mô tả cụ thể phương pháp tôi đã làm cũng như đưa các đoạn chương trình tôi thực hiện để có thể hoạt động được.

Phát hiện những IP đang thực hiện bruteforce

Sử dụng phương pháp kiểm tra các kết nối đang mở bằng netstat

netstat là một tiện ích của linux cho phép chúng ta liệt kê tất cả các thông tin liên quan tới các kết nối tới máy tính. Chúng ta có thể kiểm tra các kết nối đang bruteforce tới máy tính bằng lệnh:

1
netstat -nat |  grep -v LISTEN | grep ":22"

Lệnh này sẽ liệt kê tất cả các kết nối TCP, lọc ra các kết nối tới cổng 22 (cổng mặc định của dịch vụ SSH). Do các máy bruteforce thường chỉ kết nối tới cổng mặc định nên các thao tác này đã khá đủ.

Sử dụng phương pháp kiểm tra log auth.log

“auth.log” là log chứa các thông tin khi có người dùng đăng nhập vào hệ thống. Đăng nhập thông qua SSH cũng sẽ được lưu vào đây. Chúng ta có thể kiểm tra các thông tin này bằng một lệnh đơn giản sau:

1
cat /var/log/auth.log | grep "authentication failures" | grep "rhost" | more

Lệnh này sẽ liệt kê toàn bộ các lượt đăng nhập sai (hoặc sai tên hoặc sai mật khẩu) thông qua SSH, và có kèm cả ngày giờ đăng nhập.

Xây dựng chương trình giả lập SSH server

Như đã trình bày trong bài viết trước, tôi có rất nhiều lựa chọn từ các dự án mã nguồn mở: Kippo hoặc Kojoney. Trong dự án này, tôi đã tự xây dựng hệ thống cho riêng mình dựa trên Twisted với thư viện Conch SSH. Các bước tôi làm như sau:

Chỉnh sửa Conch

Thư viện Conch khá hoàn thiện, cho phép chúng ta xây dựng SSH server với đầy đủ các mô tả của giao thức SSH2. Tuy nhiên, chúng ta chỉ cần ghi nhận địa chỉ của máy thực hiện bruteforce, username và password là đủ. Chúng ta chưa cần phải thực hiện giả lập các thao tác sau khi hacker đăng nhập được vào hệ thống. Do đó, chúng ta sẽ luôn luôn trả về kết quả đăng nhập không thành công. Để làm điều này, tôi sẽ patch thư hiện Conch của Twisted bằng một lớp mới (AuthServerWithPeer) của tôi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AuthServerWithPeer(userauth.SSHUserAuthServer):
    def auth_password(self, packet):
        password = getNS(packet[1:])[0] # parse nhận được trong quá trình authentication

        # Tạo một Object chứa các thông tin đăng nhập, object này phải phù hợp với interface
        # của hàm auth_password(). Hoàn toàn không hề có tài liệu mô tả chính xác cho interface này.
        # credentials.UsernamePassword là một implementation của interface nên tôi kế thừa class này
        # Để biết được interface này, tôi đã đọc mã nguồn của Conch.
        # Đây là ưu điểm của các ứng dụng mã nguồn mở do chúng ta luôn có source code của toàn bộ chương trình
        c = UsernamePasswordPeer(self.user, password, self.transport.getPeer()) 
        return self.portal.login(c, None, interfaces.IConchUser).addErrback(
            self._ebPassword)

# khởi tạo một instance của SSHFactory			
factory = SSHFactory()
# patch module SSH Authentication mặc định thành AuthServerWithPeer
factory.services[b'ssh-userauth'] = AuthServerWithPeer

UsernamePasswordPeer sẽ thực hiện ghi log khi có bất kì yêu cầu đăng nhập nào gửi tới server. Thông qua đọc mã nguồn, tôi biết được phương thức requestAvatarId sẽ nhận thông tin đăng nhập. Tôi đặt code ghi log vào đó.

Toàn bộ code của quá trình như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@implementer(ICredentialsChecker)
class UsernamePasswordLogger(object):
    """
    A simple credentials logger.
    """

    credentialInterfaces = (credentials.IUsernamePassword,
                            credentials.IUsernameHashedPassword)

    def requestAvatarId(self, user_cred):
        self.write_log(user_cred)
        return defer.fail(error.LoginFailed())

    @staticmethod
    def write_log(cred):
        # thực hiện ghi log ở đây
        pass


class UsernamePasswordPeer(credentials.UsernamePassword):
    def __init__(self, username, password, peer):
        credentials.UsernamePassword.__init__(self, username, password)
        self.peer = peer


class AuthServerWithPeer(userauth.SSHUserAuthServer):
    def auth_password(self, packet):
        password = getNS(packet[1:])[0]
        c = UsernamePasswordPeer(self.user, password, self.transport.getPeer())
        return self.portal.login(c, None, interfaces.IConchUser).addErrback(
            self._ebPassword)


class SimpleRealm(object):
    def requestAvatar(self, avatarId, mind, *i):
        user = ConchUser()
        user.channelLookup['session'] = SSHChannel

        def nothing():
            pass

        return interfaces.IConchUser, user, nothing


factory = SSHFactory()
# patch module SSH Authentication mặc định thành AuthServerWithPeer
factory.services[b'ssh-userauth'] = AuthServerWithPeer
# sửa thông tin version của SSH cho giống với 1 server thật
factory.protocol.ourVersionString = 'SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.4'
factory.privateKeys = {'ssh-rsa': privateKey}
factory.publicKeys = {'ssh-rsa': publicKey}
# thiết lập moduli file để khởi tạo cho server, nếu không thiết lập thì sẽ dùng các giá trị mặc định
if MODULI_FILE:
    factory.primes = primes.parseModuliFile(MODULI_FILE)
factory.portal = Portal(SimpleRealm())
factory.portal.registerChecker(UsernamePasswordLogger())
reactor.listenTCP(port=2022, factory=factory)
reactor.run()

Để tránh chương trình phải chạy với quyền root (do 22 là một cổng privileged port), tôi sẽ lắng nghe ở cổng 2022 sau đó dùng luật iptables để redirect kết nối từ cổng 22 về 2022.

1
 iptables -A PREROUTING -t nat  -p tcp -m tcp --dport 22 -j REDIRECT --to-ports 2022

Một cách phức tạp khác là chúng ta thêm CAP_NET_BIND_SERVICE cho tiến trình lắng nghe.

Lưu các thông tin đăng nhập

Các thông tin đăng nhập có thể kể đến là :

1
2
3
4
5
6
login_info = { 
                'host': cred.peer.address.host,
                'username': cred.username,
                'password': cred.password,
                'last_update': int(time.time())
            }

Trong đó:

Khi có các thông tin này, các bạn có thể sử dụng bất cứ một cơ sở dữ liệu nào để lưu, tôi sử dụng mongodb để lưu lại.

Xây dựng giao hiện hiển thị

Quá trình xây dựng giao hiện hiển thị khá đơn giản, gồm 2 phần là front-end và back-end

Kết quả

Tôi bắt đầu vào ngày 26 tháng 3 và tới nay đã có hơn 495000 lượt bruteforce, từ hơn 1867 ip khác nhau và sử dụng trên 90000 cặp username password. Tôi có đánh dấu tất cả các ip đó trên bản đồ. Để xem chi tiết hơn, các bạn có thể vào đường link bên dưới:

https://develbranch-lsh.herokuapp.com/lsh