Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
venv
build

__pycache__/
*.pyc
50 changes: 28 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
RPIVOT - reverse socks 4 proxy for penetration tests
===================

# RPIVOT - Reverse SOCKS4 Proxy for Penetration Tests

RPIVOT allows to tunnel traffic into internal network via socks 4. It works like ssh dynamic port forwarding but in the opposite direction.

---

----------


Description
-------------
## Description

This tool is Python 2.6-2.7 compatible and has no dependencies beyond the standard library. It has client-server architecture. Just run the client on the machine you want to tunnel the traffic through. Server should be started on pentester's machine and listen to incoming connections from the client.
This tool is **Python 3.12+** compatible and has no dependencies beyond the standard library. It has client-server architecture. Just run the client on the machine you want to tunnel the traffic through. Server should be started on pentester's machine and listen to incoming connections from the client.

Works on Kali Linux, Solaris 10, Windows, Mac OS.

> **Note**: This tool was originally Python 2.6-2.7 compatible. It has been updated to work with modern Python 3.12+.

Usage example
-------------
---

## Usage Example

Start server listener on port 9999, which creates a socks 4 proxy on 127.0.0.1:1080 upon connection from client:

`python server.py --server-port 9999 --server-ip 0.0.0.0 --proxy-ip 127.0.0.1 --proxy-port 1080`
```bash
python3 server.py --server-port 9999 --server-ip 0.0.0.0 --proxy-ip 127.0.0.1 --proxy-port 1080
```

Connect to the server:

`python client.py --server-ip <rpivot_server_ip> --server-port 9999`
```bash
python3 client.py --server-ip <rpivot_server_ip> --server-port 9999
```

To pivot through an NTLM proxy:

`python client.py --server-ip <rpivot_server_ip> --server-port 9999 --ntlm-proxy-ip <proxy_ip> --ntlm-proxy-port 8080 --domain CONTOSO.COM --username Alice --password P@ssw0rd`
```bash
python3 client.py --server-ip <rpivot_server_ip> --server-port 9999 --ntlm-proxy-ip <proxy_ip> --ntlm-proxy-port 8080 --domain CONTOSO.COM --username Alice --password P@ssw0rd
```

Pass-the-hash is supported:

`python client.py --server-ip <rpivot_server_ip> --server-port 9999 --ntlm-proxy-ip <proxy_ip> --ntlm-proxy-port 8080 --domain CONTOSO.COM --username Alice --hashes 9b9850751be2515c8231e5189015bbe6:49ef7638d69a01f26d96ed673bf50c45`
```bash
python3 client.py --server-ip <rpivot_server_ip> --server-port 9999 --ntlm-proxy-ip <proxy_ip> --ntlm-proxy-port 8080 --domain CONTOSO.COM --username Alice --hashes 9b9850751be2515c8231e5189015bbe6:49ef7638d69a01f26d96ed673bf50c45
```

You can use `proxychains` to tunnel traffic through socks proxy.

Expand All @@ -49,19 +54,20 @@ socks4 127.0.0.1 1080

Using single zip file mode:

```
```bash
zip rpivot.zip -r *.py ./ntlm_auth/
python rpivot.zip server <server_options>
python rpivot.zip client <client_options>
python3 rpivot.zip server <server_options>
python3 rpivot.zip client <client_options>
```

Pivot and have fun:

`proxychains <tool_name>`
```bash
proxychains <tool_name>
```

Pre-built Windows client binary available in release section.
---

Author
------
## Author

Artem Kondratenko https://twitter.com/artkond
107 changes: 61 additions & 46 deletions client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3

import logging
import logging.handlers
Expand All @@ -19,7 +19,7 @@


def key_by_value(my_dict, value):
for k, v in my_dict.iteritems():
for k, v in my_dict.items():
if v == value:
return k
return None
Expand Down Expand Up @@ -74,27 +74,31 @@ def run(self):

try:
time.sleep(relay.delay)
logger.debug("Active channels: {0}. Pending Channels {1}".format(self.channel.keys(), self.establishing_dict.values()))
logger.debug("Active channels: {0}. Pending Channels {1}".format(list(self.channel.keys()), list(self.establishing_dict.values())))
inputready, outputready, exceptready = select.select(self.input_list, self.establishing_dict.keys(), [], 15)
except KeyboardInterrupt:
logger.info('SIGINT received. Closing relay and exiting')
self.send_remote_cmd(self.bc_sock, relay.CLOSE_RELAY)
self.shutdown()
except select.error as (code, msg):
logger.debug('Select error on select. Errno: {0} Msg: {1}'.format(errno.errorcode[code], msg))
except select.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug('Select error on select. Errno: {0} Msg: {1}'.format(errno.errorcode.get(code, code), msg))
self.shutdown()
except socket.error as (code, msg):
logger.debug('Socket error on select. Errno: {0} Msg: {1}'.format(errno.errorcode[code], msg))
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug('Socket error on select. Errno: {0} Msg: {1}'.format(errno.errorcode.get(code, code), msg))
self.shutdown()

for sock in outputready:
channel_id = self.establishing_dict[sock]
logger.debug('Establishing connection with channel id {0}'.format(channel_id))
try:
sock.recv(0)
except socket.error as (code, err_msg):
except socket.error as e:
code = e.args[0] if e.args else 0
err_msg = e.args[1] if len(e.args) > 1 else str(e)
if code == errno.ECONNREFUSED or code == errno.ETIMEDOUT:
logger.debug('Connection {0}'.format(errno.errorcode[code]))
logger.debug('Connection {0}'.format(errno.errorcode.get(code, code)))

if sock in inputready:
inputready.remove(sock)
Expand Down Expand Up @@ -131,7 +135,7 @@ def run(self):
self.manage_forward_socket(self.selected_input_socket)

def handle_remote_cmd(self, data):
cmd = data[0]
cmd = data[0:1]
logger.debug('Received cmd data from remote side. Cmd: {0}'.format(relay.cmd_names[cmd]))
if cmd == relay.CHANNEL_CLOSE_CMD:
channel_id = unpack('<H', data[1:3])[0]
Expand Down Expand Up @@ -168,9 +172,10 @@ def get_channel_data(self, sock):
tlv_header = relay.recvall(sock, 4)
channel_id, tlv_data_len = unpack('<HH', tlv_header)
data = relay.recvall(sock, tlv_data_len)
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug('Exception on receiving tlv message from remote side. Exiting')
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode[code], msg))
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode.get(code, code), msg))
raise relay.RelayError

return channel_id, data
Expand All @@ -188,7 +193,7 @@ def manage_remote_socket(self, sock):
elif channel_id in self.channel:
relay_to_sock = self.channel[channel_id]
logger.debug('Got data to relay from remote side. Channel id {0}. Data length: {1}'.format(channel_id, len(data)))
logger.debug('Data contents: {0}'.format(data.encode('hex')))
logger.debug('Data contents: {0}'.format(data.hex()))
self.relay(data, relay_to_sock)
else:
logger.debug('Relay from socket {0} with channel {1} not possible. Channel does not exist'.format(sock, channel_id))
Expand All @@ -206,9 +211,10 @@ def manage_forward_socket(self, sock):
#logger.debug('Readable socket {0} with channel id {1}'.format(sock, channel_id))
try:
data = sock.recv(relay.buffer_size)
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug('Exception on receiving data from socket {0} with channel id {1}'.format(sock, channel_id))
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode[code], msg))
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode.get(code, code), msg))
logger.debug('Closing socket {0} with channel id {1}'.format(sock, channel_id))
self.close_forward_connection(sock)
return
Expand All @@ -220,8 +226,8 @@ def manage_forward_socket(self, sock):
channel_id = self.id_by_socket[sock]
tlv_header = pack('<HH', channel_id, len(data))
logger.debug('Got data to relay from app side. Channel id {0}. Data length: {1}'.format(channel_id, len(data)))
logger.debug('Preparing tlv header: {0}'.format(tlv_header.encode('hex')))
logger.debug('Data contents: {0}'.format(data.encode('hex')))
logger.debug('Preparing tlv header: {0}'.format(tlv_header.hex()))
logger.debug('Data contents: {0}'.format(data.hex()))
self.relay(tlv_header + data, self.bc_sock)

def close_forward_connection(self, sock):
Expand Down Expand Up @@ -250,8 +256,10 @@ def send_remote_cmd(self, sock, cmd, *args):
tlv_header = pack('<HH', relay.COMMAND_CHANNEL, len(cmd_buffer))
try:
sock.send(tlv_header + cmd_buffer)
except socket.error as (code, cmd):
logger.error('Socket error on sending command to remote side. Code {0}. Msg {1}'.format(code, cmd))
except socket.error as e:
code = e.args[0] if e.args else 0
cmd_err = e.args[1] if len(e.args) > 1 else str(e)
logger.error('Socket error on sending command to remote side. Code {0}. Msg {1}'.format(code, cmd_err))

def set_channel(self, sock, channel_id):
self.channel[channel_id] = sock
Expand All @@ -267,7 +275,8 @@ def establish_forward_socket(self, channel_id, host, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)
sock.connect_ex((host, port))
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug("Caught exception socket.error during establishing forward connection. Code {0}. Msg {1}".format(code, msg))
self.send_remote_cmd(self.bc_sock, relay.FORWARD_CONNECTION_FAILURE, channel_id)
return
Expand All @@ -279,9 +288,10 @@ def relay(self, data, to_socket):
return
try:
to_socket.send(data)
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug('Exception on relaying data to socket {0}'.format(to_socket))
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode[code], msg))
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode.get(code, code), msg))
if to_socket == self.bc_sock:
raise relay.RelayError
else:
Expand All @@ -295,19 +305,19 @@ def relay(self, data, to_socket):

class NtlmProxyContext(object):

negotiate_request = '''CONNECT {0}:{1} HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0
Proxy-Connection: keep-alive
Connection: keep-alive
Proxy-Authorization: NTLM {2}

negotiate_request = '''CONNECT {0}:{1} HTTP/1.1\r
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0\r
Proxy-Connection: keep-alive\r
Connection: keep-alive\r
Proxy-Authorization: NTLM {2}\r
\r
'''
authenticate_request = '''CONNECT {0}:{1} HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0
Proxy-Connection: keep-alive
Connection: keep-alive
Proxy-Authorization: NTLM {2}

authenticate_request = '''CONNECT {0}:{1} HTTP/1.1\r
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0\r
Proxy-Connection: keep-alive\r
Connection: keep-alive\r
Proxy-Authorization: NTLM {2}\r
\r
'''

def __init__(self, sock, proxy_ip, proxy_port, username, domain=None, password=None, nthash=None, lmhash=None):
Expand All @@ -328,17 +338,18 @@ def connect(self, host_port):
resp = None
try:
self._sock.connect((self._proxy_ip, self._proxy_port))
self._sock.send(NtlmProxyContext.negotiate_request.format(host, str(port), negotiate_message))
resp = self._sock.recv(4096)
except socket.error as (code, msg):
self._sock.send(NtlmProxyContext.negotiate_request.format(host, str(port), negotiate_message).encode())
resp = self._sock.recv(4096).decode('utf-8', errors='replace')
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.error("Caught socket error trying to establish connection to proxy. Code {0}. Msg {1}".format(code, msg))
raise

try:
chal_msg = NtlmProxyContext.get_challenge(resp)
ntlm_context.parse_challenge_message(chal_msg)
except TypeError:
logger.error("Couldn't parse proxy challenge. Code {0}. Msg {1}".format(code, msg))
logger.error("Couldn't parse proxy challenge.")
if resp is not None:
logger.error("Challenge contents: {0}".format(resp))
else:
Expand All @@ -354,9 +365,10 @@ def connect(self, host_port):
lmhash=self._lmhash).decode()
resp = None
try:
self._sock.send(NtlmProxyContext.authenticate_request.format(host, str(port), authenticate_message))
resp = self._sock.recv(4096)
except socket.error as (code, msg):
self._sock.send(NtlmProxyContext.authenticate_request.format(host, str(port), authenticate_message).encode())
resp = self._sock.recv(4096).decode('utf-8', errors='replace')
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.error("Caught socket error trying to send challenge response connection to proxy. Code {0}. Msg {1}".format(code, msg))
self._sock.close()
raise
Expand Down Expand Up @@ -421,7 +433,7 @@ def main():

cmd_options = parser.parse_args()[0]
if cmd_options.server_ip is None:
print 'Server IP required'
print('Server IP required')
sys.exit()
logger = logging.getLogger('root')
logger.setLevel(logging.DEBUG)
Expand Down Expand Up @@ -466,7 +478,8 @@ def main():
bc_sock = ntlm_con
bc_sock.connect((backconnect_host, backconnect_port))
break
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.info('Unable to connect to {0} port: {1}. Caught socket error trying to establish connection with RPIVOT server. Code {2}. Msg {3}'.format(cmd_options.server_ip, cmd_options.server_port, code, msg))
logger.info('Retrying')
time.sleep(5)
Expand All @@ -479,7 +492,8 @@ def main():
bc_sock.close()
time.sleep(5)
continue
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.error("Caught socket error trying to establish connection with RPIVOT server. Code {0}. Msg {1}".format(code, msg))
bc_sock.close()
time.sleep(5)
Expand All @@ -488,9 +502,10 @@ def main():
socks_relayer = SocksRelay(bc_sock)
try:
socks_relayer.run()
except socket.error as (code, msg):
except socket.error as e:
code, msg = e.args if len(e.args) >= 2 else (e.args[0] if e.args else 0, str(e))
logger.debug('Exception in socks_relayer.run(). Restarting relay')
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode[code], msg))
logger.debug('Errno: {0} Msg: {1}'.format(errno.errorcode.get(code, code), msg))
bc_sock.close()
continue
time.sleep(10)
Expand Down
23 changes: 21 additions & 2 deletions ntlm_auth/compute_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _lmowfv1(password, lmhash):

# fix the password length to 14 bytes
if lmhash is not None:
return lmhash.decode('hex')
return bytes.fromhex(lmhash)

password = password.upper()
lm_pw = password[0:14]
Expand Down Expand Up @@ -60,7 +60,7 @@ def _ntowfv1(password, nthash):
:return digest: An NT hash of the password supplied
"""
if nthash is not None:
return nthash.decode('hex')
return bytes.fromhex(nthash)

digest = hashlib.new('md4', password.encode('utf-16le')).digest()
return digest
Expand All @@ -82,4 +82,23 @@ def _ntowfv2(user_name, password, nthash, domain_name):
digest = hmac.new(digest, (user_name.upper() + domain_name).encode('utf-16le')).digest()


return digest

def _ntowfv2(user_name, password, nthash, domain_name):
"""
[MS-NLMP] v28.0 2016-07-14

3.3.2 NTLM v2 Authentication
Same function as NTOWFv2 (and LMOWFv2) in document to create a one way hash of the password.
This combines some extra security features over the v1 calculations used in NTLMv2 auth.

:param user_name: The user name of the user we are trying to authenticate with
:param password: The password of the user we are trying to authenticate with
:param domain_name: The domain name of the user account we are authenticated with
:return digest: An NT hash of the parameters supplied
"""
digest = _ntowfv1(password, nthash)
digest = hmac.new(digest, (user_name.upper() + domain_name).encode('utf-16le')).digest()


return digest
Loading