summaryrefslogtreecommitdiffstats
path: root/scripts/deluge-rpc-brute.nse
blob: d92b004f6e134c2f5f488990984ce9bb701c9f15 (plain)
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
local brute = require "brute"
local creds = require "creds"
local shortport = require "shortport"
local string = require "string"

local have_zlib, zlib = pcall(require, "zlib")

description = [[
Performs brute force password auditing against the DelugeRPC daemon.
]]

---
-- @usage
-- nmap --script deluge-rpc-brute -p 58846 <host>
--
-- @output
-- PORT      STATE SERVICE REASON  TTL
-- 58846/tcp open  unknown syn-ack 0
-- | deluge-rpc-brute:
-- |   Accounts
-- |     admin:default - Valid credentials
-- |   Statistics
-- |_    Performed 8 guesses in 1 seconds, average tps: 8

author = "Claudiu Perta <claudiu.perta@gmail.com>"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}

portrule = shortport.port_or_service(58846, "deluge-rpc")

-- Returns an rencoded login request with the given username and password.
-- The format of the login command is the following:
--
-- ((0, 'daemon.login', ('username', 'password'), {}),)
--
-- This is inspired from deluge source code, in particular, see
-- http://git.deluge-torrent.org/deluge/tree/deluge/rencode.py
local rencoded_login_request = function(username, password)
  local INT_POS_FIXED_START = 0
  local INT_POS_FIXED_COUNT = 44

  -- Dictionaries with length embedded in typecode.
  local DICT_FIXED_START = 102
  local DICT_FIXED_COUNT = 25

  -- Strings with length embedded in typecode.
  local STR_FIXED_START = 128
  local STR_FIXED_COUNT = 64

  -- Lists with length embedded in typecode.
  local LIST_FIXED_START = 192
  local LIST_FIXED_COUNT = 64

  if #username > 0xff - STR_FIXED_START then
    return nil, "Username too long"
  elseif #password > 0xff - STR_FIXED_START then
    return nil, "Password too long"
  end

  -- Encode the login request:
  -- ((0, 'daemon.login', ('username', 'password'), {}),)
  local request = string.pack("BBBB",
    LIST_FIXED_START + 1,
    LIST_FIXED_START + 4,
    INT_POS_FIXED_START,
    STR_FIXED_START + string.len("daemon.login")
    )
  .. "daemon.login"
  .. string.pack("BB",
    LIST_FIXED_START + 2,
    STR_FIXED_START + string.len(username)
    )
  ..  username
  .. string.pack("B",
    STR_FIXED_START + string.len(password)
    )
  ..  password
  .. string.pack("B", DICT_FIXED_START)

  return request
end

Driver = {

  new = function(self, host, port, invalid_users)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.host = host
    o.port = port
    o.invalid_users = invalid_users
    return o
  end,

  connect = function(self)
    local status, err
    self.socket = brute.new_socket()
    self.socket:set_timeout(
      ((self.host.times and self.host.times.timeout) or 8) * 1000)

    local status, err = self.socket:connect(self.host, self.port, "ssl")
    if not status then
      return false, brute.Error:new("Failed to connect to server")
    end

    return true
  end,

  disconnect = function(self)
    self.socket:close()
  end,

  login = function(self, username, password)
    if (self.invalid_users[username]) then
      return false, brute.Error:new("Invalid user")
    end

    local request, err = rencoded_login_request(username, password)
    if not request then
      return false, brute.Error:new(err)
    end
    local status, err = self.socket:send(zlib.compress(request))

    if not status then
      return false, brute.Error:new("Login error")
    end

    local status, response = self.socket:receive()
    if not status  then

      return false, brute.Error:new("Login error")
    end

    response = zlib.decompress(response)
    if response:match("BadLoginError") then
      local error_message = "Login error"
      if response:match("Username does not exist") then
        self.invalid_users[username] = true
        error_message = "Username not found"
      elseif response:match("Password does not match") then
        error_message = "Username not found"
      end
      return false, brute.Error:new(error_message)
    end

    return true, creds.Account:new(username, password, creds.State.VALID)
  end,

  check = function(self)
    return true
  end
}

action = function(host, port)

  if not have_zlib then
    return "Error: zlib required!"
  end

  local invalid_users = {}
  local engine = brute.Engine:new(Driver, host, port, invalid_users)

  engine.options.script_name = SCRIPT_NAME
  local status, results = engine:start()

  return results
end