<#
.SYNOPSIS

Powershell Socks Proxy

Author: p3nt4 (https://twitter.com/xP3nt4)
License: MIT

.DESCRIPTION

Creates a local or "reverse" Socks proxy using powershell.

Supports both Socks4 and Socks5 connections.
This is only a subset of the Socks 4 and 5 protocols: It does not support authentication, It does not support UDP or bind requests.

New features will be implemented in the future. PRs are welcome.

 .EXAMPLE_LOCAL

# Create a Socks proxy on port 1234:
Invoke-SocksProxy -bindPort 1234
# Change the number of threads from 200 to 400:
Invoke-SocksProxy -bindPort 1234 -threads 400

 .EXAMPLE_REVERSE

# On the remote host:
# Generate a private key and self signed cert
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout private.key -out cert.pem
# Get the certificate fingerprint to verify it:
openssl x509 -in cert.pem -noout -sha1 -fingerprint | cut -d "=" -f 2 | tr -d ":"
# Start the handler
python ReverseSocksProxyHandler.py 443 1080 ./cert.pem ./private.key

# On the local host:
Import-Module .\Invoke-SocksProxy.psm1
Invoke-ReverseSocksProxy -remotePort 443 -remoteHost 192.168.49.130
# Go through the system proxy:
Invoke-ReverseSocksProxy -remotePort 443 -remoteHost 192.168.49.130 -useSystemProxy
# Validate certificate
Invoke-ReverseSocksProxy -remotePort 443 -remoteHost 192.168.49.130 -certFingerprint '93061FDB30D69A435ACF96430744C5CC5473D44E'
# Give up after a number of failed connections to the handler:
Invoke-ReverseSocksProxy -remotePort 443 -remoteHost 192.168.49.130 -maxRetries 10
#>



[ScriptBlock]$SocksConnectionMgr = {
    param($vars)
    $Script = {
            param($vars)
            $vars.inStream.CopyTo($vars.outStream)
            Exit
    }
    $rsp=$vars.rsp;
    function Get-IpAddress{
        param($ip)
        IF ($ip -as [ipaddress]){
            return $ip
        }else{
            $ip2 = [System.Net.Dns]::GetHostAddresses($ip)[0].IPAddressToString;
        }
        return $ip2
    }
    $client=$vars.cliConnection
    $buffer = New-Object System.Byte[] 32
    try
    {
        $cliStream = $vars.cliStream
        $cliStream.Read($buffer,0,2) | Out-Null
        $socksVer=$buffer[0]
        if ($socksVer -eq 5){
            $cliStream.Read($buffer,2,$buffer[1]) | Out-Null
            for ($i=2; $i -le $buffer[1]+1; $i++) {
                if ($buffer[$i] -eq 0) {break}
            }
            if ($buffer[$i] -ne 0){
                $buffer[1]=255
                $cliStream.Write($buffer,0,2)
            }else{
                $buffer[1]=0
                $cliStream.Write($buffer,0,2)
            }
            $cliStream.Read($buffer,0,4) | Out-Null
            $cmd = $buffer[1]
            $atyp = $buffer[3]
            if($cmd -ne 1){
                $buffer[1] = 7
                $cliStream.Write($buffer,0,2)
                throw "Not a connect"
            }
            if($atyp -eq 1){
                $ipv4 = New-Object System.Byte[] 4
                $cliStream.Read($ipv4,0,4) | Out-Null
                $ipAddress = New-Object System.Net.IPAddress(,$ipv4)
                $hostName = $ipAddress.ToString()
            }elseif($atyp -eq 3){
                $cliStream.Read($buffer,4,1) | Out-Null
                $hostBuff = New-Object System.Byte[] $buffer[4]
                $cliStream.Read($hostBuff,0,$buffer[4]) | Out-Null
                $hostName = [System.Text.Encoding]::ASCII.GetString($hostBuff)
            }
            else{
                $buffer[1] = 8
                $cliStream.Write($buffer,0,2)
                throw "Not a valid destination address"
            }
            $cliStream.Read($buffer,4,2) | Out-Null
            $destPort = $buffer[4]*256 + $buffer[5]
            $destHost = Get-IpAddress($hostName)
            if($destHost -eq $null){
                $buffer[1]=4
                $cliStream.Write($buffer,0,2)
                throw "Cant resolve destination address"
            }
            $tmpServ = New-Object System.Net.Sockets.TcpClient($destHost, $destPort)
            if($tmpServ.Connected){
                $buffer[1]=0
                $buffer[3]=1
                $buffer[4]=0
                $buffer[5]=0
                $cliStream.Write($buffer,0,10)
                $cliStream.Flush()
                $srvStream = $tmpServ.GetStream()
                $AsyncJobResult2 = $srvStream.CopyToAsync($cliStream)
                $AsyncJobResult = $cliStream.CopyToAsync($srvStream)
                $AsyncJobResult.AsyncWaitHandle.WaitOne();
                $AsyncJobResult2.AsyncWaitHandle.WaitOne();

            }
            else{
                $buffer[1]=4
                $cliStream.Write($buffer,0,2)
                throw "Cant connect to host"
            }
       }elseif($socksVer -eq 4){
            $cmd = $buffer[1]
            if($cmd -ne 1){
                $buffer[0] = 0
                $buffer[1] = 91
                $cliStream.Write($buffer,0,2)
                throw "Not a connect"
            }
            $cliStream.Read($buffer,2,2) | Out-Null
            $destPort = $buffer[2]*256 + $buffer[3]
            $ipv4 = New-Object System.Byte[] 4
            $cliStream.Read($ipv4,0,4) | Out-Null
            $destHost = New-Object System.Net.IPAddress(,$ipv4)
            $buffer[0]=1
            while ($buffer[0] -ne 0){
                $cliStream.Read($buffer,0,1)
            }
            $tmpServ = New-Object System.Net.Sockets.TcpClient($destHost, $destPort)

            if($tmpServ.Connected){
                $buffer[0]=0
                $buffer[1]=90
                $buffer[2]=0
                $buffer[3]=0
                $cliStream.Write($buffer,0,8)
                $cliStream.Flush()
                $srvStream = $tmpServ.GetStream()
                $AsyncJobResult2 = $srvStream.CopyToAsync($cliStream)
                $AsyncJobResult = $cliStream.CopyTo($srvStream)
                $AsyncJobResult.AsyncWaitHandle.WaitOne();
                $AsyncJobResult2.AsyncWaitHandle.WaitOne();
            }
       }else{
            throw "Unknown socks version"
       }
    }
    catch {
        #$_ >> "error.log"
    }
    finally {
        if ($client -ne $null) {
            $client.Dispose()
        }
        if ($tmpServ -ne $null) {
            $tmpServ.Dispose()
        }
        Exit;
    }
}


function Invoke-SocksProxy{
    param (

            [String]$bindIP = "0.0.0.0",

            [Int]$bindPort = 1080,

            [Int]$threads = 200

     )
    try{
        $listener = new-object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Parse($bindIP), $bindPort)
        $listener.start()
        $rsp = [runspacefactory]::CreateRunspacePool(1,$threads);
        $rsp.CleanupInterval = New-TimeSpan -Seconds 30;
        $rsp.open();
        write-host "Listening on port $bindPort..."
        while($true){
            $client = $listener.AcceptTcpClient()
            $cliStream = $client.GetStream()
            Write-Host "New Connection from " $client.Client.RemoteEndPoint
            $vars = [PSCustomObject]@{"cliConnection"=$client; "rsp"=$rsp; "cliStream" = $cliStream}
            $PS3 = [PowerShell]::Create()
            $PS3.RunspacePool = $rsp;
            $PS3.AddScript($SocksConnectionMgr).AddArgument($vars) | Out-Null
            $PS3.BeginInvoke() | Out-Null
            Write-Host "Threads Left:" $rsp.GetAvailableRunspaces()
        }
     }
    catch{
        throw $_
    }
    finally{
        write-host "Server closed."
        if ($listener -ne $null) {
                  $listener.Stop()
           }
        if ($client -ne $null) {
            $client.Dispose()
            $client = $null
        }
        if ($PS3 -ne $null -and $AsyncJobResult3 -ne $null) {
            $PS3.EndInvoke($AsyncJobResult3) | Out-Null
            $PS3.Runspace.Close()
            $PS3.Dispose()
        }
    }
}

# Credit to Arno0x for this technique
function getProxyConnection{

    param (

            [String]$remoteHost,

            [Int]$remotePort

     )
    #Sleep -Milliseconds 500
    $request = [System.Net.HttpWebRequest]::Create("http://" + $remoteHost + ":" + $remotePort )
    $request.Method = "CONNECT";
    $proxy = [System.Net.WebRequest]::GetSystemWebProxy();
    $proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials;
    $request.Proxy = $proxy;
    $request.timeout = 1000;
    $serverResponse = $request.GetResponse();
    $request.timeout = 100000;
    $responseStream = $serverResponse.GetResponseStream()
    $BindingFlags= [Reflection.BindingFlags] "NonPublic,Instance"
    $rsType = $responseStream.GetType()
    $connectionProperty = $rsType.GetProperty("Connection", $BindingFlags)
    $connection = $connectionProperty.GetValue($responseStream, $null)
    $connectionType = $connection.GetType()
    $networkStreamProperty = $connectionType.GetProperty("NetworkStream", $BindingFlags)
    $serverStream = $networkStreamProperty.GetValue($connection, $null)
    return $connection, $serverStream
}


## EXPERIMENTAL.....
function Invoke-ReverseSocksProxy{
    param (

            [String]$remoteHost = "127.0.0.1",

            [Int]$remotePort = 1080,

            [Switch]$useSystemProxy = $false,

            [String]$certFingerprint = "",

            [Int]$threads = 200,

            [Int]$maxRetries = 0

     )
    try{
        $currentTry = 0;
        $rsp = [runspacefactory]::CreateRunspacePool(1,$threads);
        $rsp.CleanupInterval = New-TimeSpan -Seconds 30;
        $rsp.open();
        while($true){
            Write-Host "Connecting to: " $remoteHost ":" $remotePort
            try{
                if($useSystemProxy -eq $false){
                        $client = New-Object System.Net.Sockets.TcpClient($remoteHost, $remotePort)
                        $cliStream_clear = $client.GetStream()
                    }else{
                        $ret = getProxyConnection -remoteHost $remoteHost -remotePort $remotePort
                        $client = $ret[0]
                        $cliStream_clear = $ret[1]
                }
                if($certFingerprint -eq ''){
                    $cliStream = New-Object System.Net.Security.SslStream($cliStream_clear,$false,({$true} -as[Net.Security.RemoteCertificateValidationCallback]));
                }else{
                    $cliStream = New-Object System.Net.Security.SslStream($cliStream_clear,$false,({return $args[1].GetCertHashString() -eq $certFingerprint } -as[Net.Security.RemoteCertificateValidationCallback]));
                }
                $cliStream.AuthenticateAsClient($remoteHost)
                Write-Host "Connected"
                $currentTry = 0;
                $buffer = New-Object System.Byte[] 32
                $buffer2 = New-Object System.Byte[] 122
                $FakeRequest = [System.Text.Encoding]::Default.GetBytes("GET / HTTP/1.1`nHost: "+$remoteHost+"`n`n")
                $cliStream.Write($FakeRequest,0,$FakeRequest.Length)
                $cliStream.ReadTimeout = 5000
                $cliStream.Read($buffer2,0,122) | Out-Null
                $cliStream.Read($buffer,0,5) | Out-Null
                $message = [System.Text.Encoding]::ASCII.GetString($buffer)
                if($message -ne "HELLO"){
                    throw "No Client connected";
                }else{
                    Write-Host "Connection received"
                }
                $cliStream.ReadTimeout = 100000;
                $vars = [PSCustomObject]@{"cliConnection"=$client; "rsp"=$rsp; "cliStream" = $cliStream}
                $PS3 = [PowerShell]::Create()
                $PS3.RunspacePool = $rsp;
                $PS3.AddScript($SocksConnectionMgr).AddArgument($vars) | Out-Null
                $PS3.BeginInvoke() | Out-Null
                Write-Host "Threads Left:" $rsp.GetAvailableRunspaces()
            }catch{
                $currentTry = $currentTry + 1;
                if (($maxRetries -ne 0) -and ($currentTry -eq $maxRetries)){
                    Throw "Cannot connect to handler, max Number of attempts reached, exiting";
                }
                if ($_.Exception.message -eq 'Exception calling "AuthenticateAsClient" with "1" argument(s): "The remote certificate is invalid according to the validation procedure."'){
                    throw $_
                }
                if ($_.Exception.message -eq 'Exception calling "AuthenticateAsClient" with "1" argument(s): "Authentication failed because the remote party has closed the transport stream."'){
                    sleep 5
                }

                if (($_.Exception.Message.Length -ge 121) -and $_.Exception.Message.substring(0,120) -eq 'Exception calling ".ctor" with "2" argument(s): "No connection could be made because the target machine actively refused'){
                    sleep 5
                }
                try{
                    $client.Close()
                    $client.Dispose()
                }catch{}
                    sleep -Milliseconds 200
                }
        }
     }
    catch{
        throw $_;
    }
    finally{
        write-host "Server closed."
        if ($client -ne $null) {
            $client.Dispose()
            $client = $null
        }
        if ($PS3 -ne $null -and $AsyncJobResult3 -ne $null) {
            $PS3.EndInvoke($AsyncJobResult3) | Out-Null
            $PS3.Runspace.Close()
            $PS3.Dispose()
        }
    }
}



function Get-IpAddress{
    param($ip)
    IF ($ip -as [ipaddress]){
        return $ip
    }else{
        $ip2 = [System.Net.Dns]::GetHostAddresses($ip)[0].IPAddressToString;
        Write-Host "$ip resolved to $ip2"
    }
    return $ip2
}
