diff --git a/CHANGELOG b/CHANGELOG
index 1eb20c522..428ba39fb 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*-
+o [NSE] Added rusers script to get logged-on users info from the rusersd RPC
+ service. [Daniel Miller]
+
o [NSE][GH#322] Added http-apache-server-status for parsing the server status
page of Apache's mod_status. [Eric Gershman]
diff --git a/scripts/rusers.nse b/scripts/rusers.nse
new file mode 100644
index 000000000..11048e983
--- /dev/null
+++ b/scripts/rusers.nse
@@ -0,0 +1,175 @@
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+
+description = [[
+Connects to rusersd RPC service and retrieves a list of logged-in users.
+]]
+
+---
+--@output
+--| USER ON FROM SINCE IDLE
+--| LOGIN console 2015-11-08T12:03:50 8h55m58s
+--| root console :0 2015-11-08T12:06:49 8h55m58s
+--| root pts/2 :0.0 2015-11-08T12:07:06 2d02h51m48s
+--| .telnet /dev/pts 2016-03-14T12:07:46 24855d03h14m07s
+--| .telnet /dev/pts 2016-03-14T10:25:09 24855d03h14m07s
+--| .telnet /dev/pts 2016-03-03T10:02:15 24855d03h14m07s
+--| root pts/4 2016-03-07T09:21:14 1m48s
+--| root pts/3 ns3 2016-02-16T09:45:24 35s
+--| root pts/4 ns3 2016-02-16T09:26:01 1m48s
+--|_.telnet /dev/pts 2016-03-03T10:01:32 24855d03h14m07s
+--
+--@xmloutput
+--
+-- 1m49s
+-- ns3
+-- root
+-- 2016-02-16T09:26:01
+-- pts/4
+--
+--
+-- 24855d03h14m07s
+--
+-- .telnet
+-- 2016-03-03T10:01:32
+-- /dev/pts
+--
+--
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+dependencies = {"rpc-grind", "rpcinfo"}
+portrule = shortport.service("rusersd", {"tcp", "udp"})
+
+-- TODO: Support version 3
+rpc.RPC_version["rusersd"] = rpc.RPC_version["rusersd"] or { min=2, max=2 }
+
+local RUSERSPROC = {
+ NUM = 1,
+ NAMES = 2,
+ ALLNAMES = 3,
+}
+
+--- Get a RPC string, which is length-prefixed and padded with null bytes
+-- @param comm an rpc.Comm object
+-- @param data the data received so far
+-- @param pos the current position in the data where the opaque string is
+-- @param additional number of bytes to request after the string for the next
+-- field. Saves a call to GetAdditionalBytes later.
+-- @return position of next field or nil on error
+-- @return the string extracted or error message
+-- @return the data retrieved so far
+local function get_zstring (comm, data, pos, additional)
+ local pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ local status, data = comm:GetAdditionalBytes( data, pos, len + additional )
+ if not status then
+ return nil, "GetAdditionalBytes failed"
+ end
+ local pos, rval = rpc.Util.unmarshall_opaque(len, data, pos)
+ rval = string.match(rval, "^(.-)\0*$")
+ return pos, rval, data
+end
+
+local function fail (err, ...)
+ stdnse.debug1(err, ...)
+ return nil
+end
+
+-- Extract a utmpidle structure:
+-- /*
+-- * This is the structure used in version 2 of the rusersd RPC service.
+-- * It corresponds to the utmp structure for BSD systems.
+-- */
+-- struct ru_utmp {
+-- char ut_line[8]; /* tty name */
+-- char ut_name[8]; /* user id */
+-- char ut_host[16]; /* host name, if remote */
+-- long int ut_time; /* time on */
+-- };
+--
+-- struct utmpidle {
+-- struct ru_utmp ui_utmp;
+-- unsigned int ui_idle;
+-- };
+local function rusers2_entry(comm, data, pos)
+ local entry = {}
+ pos, entry.tty, data = get_zstring(comm, data, pos, 4)
+ if not pos then return fail(entry.tty) end
+
+ pos, entry.user, data = get_zstring(comm, data, pos, 4)
+ if not pos then return fail(entry.user) end
+
+ pos, entry.host, data = get_zstring(comm, data, pos, 8)
+ if not pos then return fail(entry.host) end
+
+ pos, entry.time = rpc.Util.unmarshall_uint32(data, pos)
+ entry.time = stdnse.format_timestamp(entry.time)
+
+ pos, entry.idle = rpc.Util.unmarshall_uint32(data, pos)
+ entry.idle = stdnse.format_time(entry.idle)
+
+ return pos, entry, data
+end
+
+action = function(host, port)
+ local comm = rpc.Comm:new("rusersd", 2)
+ local status, err = comm:Connect(host, port)
+ if not status then
+ return fail("RPC connect error: %s", err)
+ end
+
+ local packet = comm:EncodePacket(nil, RUSERSPROC.ALLNAMES, {type = rpc.Portmap.AuthType.NULL}, nil)
+ status, err = comm:SendPacket(packet)
+ if not status then
+ return fail("RPC send error: %s", err)
+ end
+
+ local status, data = comm:ReceivePacket()
+ if not status then
+ return fail("RPC receive error: %s", data)
+ end
+
+ local pos, header = comm:DecodeHeader(data, 1)
+ if not header then
+ return fail("RPC decode header error")
+ end
+
+ if header.type ~= rpc.Portmap.MessageType.REPLY then
+ return fail("Packet was not a reply")
+ end
+
+ if header.state ~= rpc.Portmap.State.MSG_ACCEPTED then
+ return fail("RPC call failed: %s", rpc.Portmap.RejectMsg[header.denied_state] or header.state)
+ end
+
+ if header.accept_state ~= rpc.Portmap.AcceptState.SUCCESS then
+ return fail("RPC accepted state: %s", rpc.Portmap.AcceptMsg[header.accept_state] or header.accept_state)
+ end
+
+ status, data = comm:GetAdditionalBytes( data, pos, 4 )
+ if not status then
+ return fail("Failed to call GetAdditionalBytes")
+ end
+
+ local pos, num_names = rpc.Util.unmarshall_uint32(data, pos)
+
+ local out = {}
+ local out_tab = tab.new()
+ tab.addrow(out_tab, "USER", "ON", "FROM", "SINCE", "IDLE")
+ for i=1, num_names do
+ local entry
+ pos, entry, data = rusers2_entry(comm, data, pos)
+ tab.addrow(out_tab, entry.user, entry.tty, entry.host, entry.time, entry.idle)
+ out[#out+1] = entry
+ end
+
+ if next(out) then
+ return out, "\n" .. tab.dump(out_tab)
+ end
+
+end
diff --git a/scripts/script.db b/scripts/script.db
index 216492929..7a98235fd 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -407,6 +407,7 @@ Entry { filename = "rsync-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "rsync-list-modules.nse", categories = { "discovery", "safe", } }
Entry { filename = "rtsp-methods.nse", categories = { "default", "safe", } }
Entry { filename = "rtsp-url-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "rusers.nse", categories = { "discovery", "safe", } }
Entry { filename = "s7-info.nse", categories = { "discovery", "version", } }
Entry { filename = "samba-vuln-cve-2012-1182.nse", categories = { "intrusive", "vuln", } }
Entry { filename = "servicetags.nse", categories = { "default", "discovery", "safe", } }